From 37e76e0aaca5226beda7d476170dd3d5be256cfb Mon Sep 17 00:00:00 2001 From: Omolemo Lethuloe Date: Mon, 25 Nov 2024 10:35:25 +0200 Subject: [PATCH 1/5] Initial commit --- .../Configuration/Email/EmailSettings.cs | 25 ++ .../Email/Gateways/SmtpSettings.cs | 59 +++ .../Email/IEmailGatewaySettings.cs | 25 ++ .../Configuration/INotificationSettings.cs | 36 ++ .../NotificationGatewaySettingNames.cs | 15 + .../Configuration/NotificationSettingNames.cs | 17 + .../Configuration/NotificationSettings.cs | 33 ++ .../Sms/Gateways/ClickatellSettings.cs | 58 +++ .../Configuration/Sms/ISmsGatewaySettings.cs | 26 ++ .../Configuration/Sms/SmsSettings.cs | 30 ++ .../Dto/BroadcastNotificationJobArgs.cs | 22 ++ .../Dto/DirectNotificationJobArgs.cs | 19 + .../Dto/NotificationMessageDto.cs | 16 + .../Emails/EmailChannelSender.cs | 106 ++++++ .../Emails/Gateways/EmailGatewayFactory.cs | 29 ++ .../Emails/Gateways/IEmailGateway.cs | 17 + .../Emails/Gateways/SmtpGateway.cs | 172 +++++++++ .../OmoNotifications/Helpers/MobileHelper.cs | 32 ++ .../Helpers/TemplateHelper.cs | 33 ++ .../INotificationChannelSender.cs | 21 ++ .../OmoNotifications/INotificationSender.cs | 15 + .../Jobs/BroadcastNotificationJobQueuer.cs | 147 ++++++++ .../Jobs/DirectNotificationJobQueuer.cs | 44 +++ .../OmoNotifications/NotificationSender.cs | 188 ++++++++++ .../OmoNotificationAppService.cs | 351 ++++++++++++++++++ .../Sms/Gateways/ClickatellGateway.cs | 72 ++++ .../Sms/Gateways/ISmsGateway.cs | 16 + .../Sms/Gateways/SmsGatewayFactory.cs | 29 ++ .../OmoNotifications/Sms/SmsChannelSender.cs | 80 ++++ .../Teams/TeamsChannelSender.cs | 34 ++ .../SheshaApplicationModule.cs | 55 ++- .../Shesha.Application/Sms/GatewaySettings.cs | 58 +++ .../Enums/RefListChannelSupportedMechanism.cs | 18 + .../Enums/RefListNotificationChannelStatus.cs | 17 + .../Enums/RefListNotificationMessageFormat.cs | 18 + .../Enums/RefListNotificationPriority.cs | 18 + .../Enums/RefListNotificationReadStatus.cs | 16 + .../src/Shesha.Core/Domain/MessageTemplate.cs | 25 ++ .../Domain/NotificationChannelConfig.cs | 65 ++++ .../Domain/NotificationGatewayConfig.cs | 47 +++ .../Shesha.Core/Domain/NotificationTopic.cs | 17 + .../Domain/NotificationTypeConfig.cs | 66 ++++ .../src/Shesha.Core/Domain/OmoNotification.cs | 52 +++ .../Domain/OmoNotificationMessage.cs | 71 ++++ .../OmoNotificationMessageAttachment.cs | 25 ++ .../Domain/UserNotificationPreference.cs | 19 + .../Domain/UserTopicSubscription.cs | 17 + .../Shesha.Core/Migrations/M20241106152600.cs | 121 ++++++ .../Shesha.Core/Migrations/M20241111155000.cs | 26 ++ .../Shesha.Core/Migrations/M20241111160500.cs | 26 ++ .../Shesha.Core/Migrations/M20241114141100.cs | 30 ++ .../Shesha.Core/Migrations/M20241114143900.cs | 31 ++ .../Shesha.Core/Migrations/M20241114153500.cs | 26 ++ .../Shesha.Core/Migrations/M20241121180900.cs | 34 ++ .../Configuration/SheshaSettingNames.cs | 4 + .../src/Shesha.Web.Host/Startup/Startup.cs | 15 + .../src/Shesha.Web.Host/appsettings.json | 2 +- 57 files changed, 2684 insertions(+), 2 deletions(-) create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs create mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs create mode 100644 shesha-core/src/Shesha.Application/Sms/GatewaySettings.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/Enums/RefListChannelSupportedMechanism.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationChannelStatus.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationMessageFormat.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationPriority.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/NotificationTopic.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/OmoNotification.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/UserNotificationPreference.cs create mode 100644 shesha-core/src/Shesha.Core/Domain/UserTopicSubscription.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs create mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs new file mode 100644 index 0000000000..0f5bf1c633 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs @@ -0,0 +1,25 @@ +using Shesha.Domain; + +namespace Shesha.OmoNotifications.Configuration.Email +{ + /// + /// Email settings + /// + public class EmailSettings + { + /// + /// If true, all emails are enabled + /// + public bool EmailsEnabled { get; set; } + + /// + /// + /// + public NotificationGatewayConfig PreferredGateway { get; set; } + + /// + /// If not null or empty the all outgoing emails will be sent to this email address, is used for testing only + /// + public string RedirectAllMessagesTo { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs new file mode 100644 index 0000000000..e7ef7d6e7d --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs @@ -0,0 +1,59 @@ +namespace Shesha.OmoNotifications.Configuration.Email.Gateways +{ + /// + /// SMTP settings + /// + public class SmtpSettings + { + /// + /// SMTP Host name/IP. + /// + public string Host { get; set; } + + /// + /// SMTP Port. + /// + public int Port { get; set; } + + /// + /// User name to login to SMTP server. + /// + public string UserName { get; set; } + + /// + /// Password to login to SMTP server. + /// + public string Password { get; set; } + + /// + /// Domain name to login to SMTP server. + /// + public string Domain { get; set; } + + /// + /// Domain name to login to Incoming server. + /// + public string IncomingServer { get; set; } + + /// + /// Is SSL enabled? + /// + public bool EnableSsl { get; set; } + + /// + /// Default from address. + /// + public string DefaultFromAddress { get; set; } + + /// + /// Default display name. + /// + public string DefaultFromDisplayName { get; set; } + + /// + /// Use SMTP relay + /// If true, indicate that SMTP relay service will be used where it's needed (e.g. if the application needs to notify one person about the action that was performed by another person then real person's email address will be used for the 'from' address, otherwise 'Site Email' will be used) + /// + public bool UseSmtpRelay { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs new file mode 100644 index 0000000000..316e34f7e1 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs @@ -0,0 +1,25 @@ +using Shesha.OmoNotifications.Configuration.Email.Gateways; +using Shesha.OmoNotifications.Configuration.Sms.Gateways; +using Shesha.Settings; +using Shesha.Sms; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Configuration.Email +{ + [Category("Email Gateways")] + public interface IEmailGatewaySettings: ISettingAccessors + { + /// + /// SMS Settings + /// + [Display(Name = "SMTP Gateway")] + [Setting(NotificationGatewaySettingNames.SmtpGatewaySettings, EditorFormName = "smtp-gateway-settings")] + ISettingAccessor SmtpSettings { get; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs new file mode 100644 index 0000000000..e41d90e2af --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs @@ -0,0 +1,36 @@ +using Shesha.OmoNotifications.Configuration.Email; +using Shesha.OmoNotifications.Configuration.Sms; +using Shesha.Settings; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Shesha.OmoNotifications.Configuration +{ + /// + /// SMS Settings + /// + [Category("Notifications")] + public interface INotificationSettings : ISettingAccessors + { + /// + /// SMS Settings + /// + [Display(Name = "Notifications Channels")] + [Setting(NotificationSettingNames.NotificationSettings, EditorFormName = "notification-settings")] + ISettingAccessor NotificationSettings { get; } + + /// + /// SMS Settings + /// + [Display(Name = "SMS Settings")] + [Setting(NotificationSettingNames.SmsSettings, EditorFormName = "sms-settings")] + ISettingAccessor SmsSettings { get; } + + /// + /// SMTP Settings + /// + [Display(Name = "Email Settings")] + [Setting(NotificationSettingNames.EmailSettings, EditorFormName = "email-settings")] + ISettingAccessor EmailSettings { get; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs new file mode 100644 index 0000000000..d53a58589a --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Configuration +{ + public static class NotificationGatewaySettingNames + { + public const string ClickatellGatewaySettings = "Shesha.Notifications.Sms.Clickatell.Settings"; + + public const string SmtpGatewaySettings = "Shesha.Notifications.Email.Smtp.Settings"; + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs new file mode 100644 index 0000000000..0d61f55663 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Configuration +{ + public static class NotificationSettingNames + { + public const string NotificationSettings = "Shesha.Notification.Settings"; + + public const string SmsSettings = "Shesha.Notification.SMS.Settings"; + + public const string EmailSettings = "Shesha.Notification.Email.Settings"; + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs new file mode 100644 index 0000000000..0894362696 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs @@ -0,0 +1,33 @@ +using Shesha.Configuration; +using Shesha.Domain; +using Shesha.Settings; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Configuration +{ + /// + /// Enter preferred channels per priority. + /// + public class NotificationSettings + { + /// + /// Urgent notifications + /// + public List Low { get; set; } + + /// + /// Normal notifications + /// + public List Medium { get; set; } + + /// + /// Informational notifications + /// + public List High { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs new file mode 100644 index 0000000000..f81dc79ffb --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; + +namespace Shesha.OmoNotifications.Configuration.Sms.Gateways +{ + /// + /// Xml2Sms gateway settings + /// + public class ClickatellSettings + { + [Display(Name = "Host")] + public string Host { get; set; } + + [Display(Name = "Api Username")] + public string Username { get; set; } + + [Display(Name = "Api Password")] + public string Password { get; set; } + + /// + /// Use proxy + /// + [Display(Name = "Use proxy")] + public bool UseProxy { get; set; } + + [Display(Name = "Proxy address")] + public string WebProxyAddress { get; set; } + + /// + /// Use default credentials for proxy + /// + [Display(Name = "Use default credentials for proxy")] + public bool UseDefaultProxyCredentials { get; set; } + + [Display(Name = "Proxy username")] + public string WebProxyUsername { get; set; } + + [Display(Name = "Proxy password")] + public string WebProxyPassword { get; set; } + + /// + /// Clickatell Api ID + /// + [Display(Name = "Clickatell Api ID")] + public string ApiId { get; set; } + + /// + /// Max length of single message (default is 160 but may be different in different regions/countries) + /// + [Display(Name = "Max length of single message")] + public int SingleMessageMaxLength { get; set; } + + /// + /// Message part length + /// + [Display(Name = "Message part length", Description = "Is used for multipart messages (153 by default)")] + public int MessagePartLength { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs new file mode 100644 index 0000000000..ccf7dac189 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs @@ -0,0 +1,26 @@ +using Shesha.Configuration; +using Shesha.OmoNotifications.Configuration.Sms.Gateways; +using Shesha.Settings; +using Shesha.Sms; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Configuration.Sms +{ + [Category("SMS Gateways")] + public interface ISmsGatewaySettings: ISettingAccessors + { + /// + /// SMS Settings + /// + [Display(Name = "Clickatell Gateway")] + [Setting(NotificationGatewaySettingNames.ClickatellGatewaySettings, EditorFormName = "clickatall-gateway-settings")] + ISettingAccessor ClickatellSettings { get; } + } +} + diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs new file mode 100644 index 0000000000..6864db6840 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs @@ -0,0 +1,30 @@ +using Shesha.Configuration; +using Shesha.Domain; +using Shesha.Settings; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Configuration.Sms +{ + public class SmsSettings + { + /// + /// If true, all Sms are enabled + /// + public bool SmsEnabled { get; set; } + /// + /// + /// + public NotificationGatewayConfig PreferredGateway { get; set; } + + /// + /// Redirect all messages to. + /// Is used for testing purposes only + /// + public string RedirectAllMessagesTo { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs new file mode 100644 index 0000000000..e711ad52b8 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs @@ -0,0 +1,22 @@ +using Abp.Notifications; +using Shesha.Domain; +using Shesha.Notifications.Dto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Dto +{ + public class BroadcastNotificationJobArgs + { + public Guid NotificationId { get; set; } + public Guid TemplateId { get; set; } + public bool HtmlSupport { get; set; } + public Guid ChannelId { get; set; } + public string Subject { get; set; } + public string Message { get; set; } + public List Attachments { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs new file mode 100644 index 0000000000..68f169e513 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs @@ -0,0 +1,19 @@ +using Shesha.Domain; +using Shesha.Notifications.Dto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Dto +{ + public class DirectNotificationJobArgs + { + public Guid FromPerson { get; set; } + public Guid ToPerson { get; set; } + public Guid Message { get; set; } + public bool HtmlSupport { get; set; } + public string SenderTypeName { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs new file mode 100644 index 0000000000..78bd750561 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs @@ -0,0 +1,16 @@ +using Shesha.Email.Dtos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Dto +{ + public class NotificationMessageDto + { + public string Subject { get; set; } + public string Message { get; set; } + public List Attachments { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs new file mode 100644 index 0000000000..5a87464a7b --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs @@ -0,0 +1,106 @@ +using Abp.Domain.Repositories; +using Castle.Core.Logging; +using DocumentFormat.OpenXml.Spreadsheet; +using DocumentFormat.OpenXml.Wordprocessing; +using NHibernate.Linq; +using Shesha.Configuration.Email; +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.Email.Dtos; +using Shesha.Notifications.Dto; +using Shesha.OmoNotifications.Configuration; +using Shesha.OmoNotifications.Configuration.Email; +using Shesha.OmoNotifications.Emails.Gateways; +using Shesha.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications +{ + public class EmailChannelSender : INotificationChannelSender + { + private readonly INotificationSettings _notificationSettings; + private readonly EmailGatewayFactory _emailGatewayFactory; + private readonly IRepository _notificationGatewayRepository; + private readonly IRepository _userTopicSubscriptionRepository; + public ILogger Logger { get; set; } = NullLogger.Instance; + + public EmailChannelSender(INotificationSettings notificationSettings, EmailGatewayFactory emailGatewayFactory, IRepository notificationGatewayRepository, IRepository userTopicSubscriptionRepository) + { + _notificationSettings = notificationSettings; + _userTopicSubscriptionRepository = userTopicSubscriptionRepository; + _emailGatewayFactory = emailGatewayFactory; + _notificationGatewayRepository = notificationGatewayRepository; + } + + public string GetRecipientId(Person person) + { + return person.EmailAddress1; + } + + public async Task GetRecipients(NotificationTopic topic) + { + var recipients = await _userTopicSubscriptionRepository.GetAll().Where(s => s.Topic.Id == topic.Id).Select(s => GetRecipientId(s.User)).ToListAsync(); + return string.Join(";", recipients); + } + + + private async Task GetSettings() + { + return await _notificationSettings.EmailSettings.GetValueAsync(); + } + + /// + /// + /// + /// + private bool EmailsEnabled() + { + var enabled = _notificationSettings.EmailSettings.GetValue().EmailsEnabled; + if (!enabled) + Logger.Warn("Emails are disabled"); + + return enabled; + } + + public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + { + //if (message.Length > MaxMessageSize) + // throw new ArgumentException("Message exceeds the maximum allowed size for Email."); + var settings = await GetSettings(); + + if (!settings.EmailsEnabled) + { + Logger.Warn("Emails are disabled"); + return await Task.FromResult(false); + } + + var preferredGateway = await _notificationGatewayRepository.GetAsync(settings.PreferredGateway.Id); + + var gateway = _emailGatewayFactory.GetGateway(preferredGateway.GatewayTypeName); + + return await gateway.SendAsync(GetRecipientId(fromPerson), GetRecipientId(toPerson), message, isBodyHtml, cc, throwException, attachments); + } + + public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) + { + var settings = await GetSettings(); + + if (!settings.EmailsEnabled) + { + Logger.Warn("Emails are disabled"); + return await Task.FromResult(false); + } + + var gateway = _emailGatewayFactory.GetGateway(settings.PreferredGateway.GatewayTypeName); + + return await gateway.BroadcastAsync(GetRecipients(topic).Result, subject, message, attachments); + } + } + +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs new file mode 100644 index 0000000000..49480d9975 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Emails.Gateways +{ + public class EmailGatewayFactory + { + private readonly IServiceProvider _serviceProvider; + + public EmailGatewayFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IEmailGateway GetGateway(string gatewayName) + { + return gatewayName switch + { + "SmtpGateway" => _serviceProvider.GetService(), + // Add other gateways here + _ => throw new NotSupportedException($"Gateway {gatewayName} is not supported.") + }; + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs new file mode 100644 index 0000000000..0b20a44eaf --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs @@ -0,0 +1,17 @@ +using Shesha.Domain; +using Shesha.Email.Dtos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Emails.Gateways +{ + public interface IEmailGateway + { + Task SendAsync(string fromPerson, string toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null); + Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); + } + +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs new file mode 100644 index 0000000000..4308fb0179 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs @@ -0,0 +1,172 @@ +using Shesha.Domain; +using Shesha.Email.Dtos; +using Shesha.OmoNotifications.Configuration.Email; +using Shesha.OmoNotifications.Configuration.Email.Gateways; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Shesha.OmoNotifications.Configuration; +using Castle.Core.Logging; +using Shesha.Utilities; + +namespace Shesha.OmoNotifications.Emails.Gateways +{ + public class SmtpGateway : IEmailGateway + { + private readonly IEmailGatewaySettings _emailGatewaySettings; + private readonly INotificationSettings _notificationSettings; + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public SmtpGateway(IEmailGatewaySettings emailGatewaySettings, INotificationSettings notificationSettings) + { + _emailGatewaySettings = emailGatewaySettings; + _notificationSettings = notificationSettings; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Task SendAsync(string fromPerson, string toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + { + // Send email via SMTP or email service provider + using (var mail = BuildMessageWith(fromPerson, toPerson, message.Subject, message.Message, isBodyHtml, cc)) + { + if (attachments != null) + { + foreach (var attachment in attachments) + { + mail.Attachments.Add(new Attachment(attachment.Stream, attachment.FileName)); + } + } + try + { + SendEmail(mail); + return Task.FromResult(true) ; + } + catch (Exception e) + { + // Log the exception + Logger.Error("Failed to send email", e); + if (throwException) + throw; + return Task.FromResult(false); + } + }; + } + + public Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null) + { + using (var mail = BuildMessageWith(null, topicSubscribers, subject, message, true)) + { + if (attachments != null) + { + foreach (var attachment in attachments) + { + mail.Attachments.Add(new Attachment(attachment.Stream, attachment.FileName)); + } + } + try + { + SendEmail(mail); + return Task.FromResult(true); + } + catch (Exception e) + { + // Log the exception + Logger.Error("Failed to send email", e); + return Task.FromResult(false); + } + }; + } + + #region private methods + + /// + /// + /// + /// + private void SendEmail(MailMessage mail) + { + using (var smtpClient = GetSmtpClient().Result) + { + smtpClient.Send(mail); + } + } + + /// + /// Returns SmtpClient configured according to the current application settings + /// + private async Task GetSmtpClient() + { + var smtpSettings = await _emailGatewaySettings.SmtpSettings.GetValueAsync(); + + var client = new SmtpClient(smtpSettings.Host, smtpSettings.Port) + { + EnableSsl = smtpSettings.EnableSsl, + Credentials = string.IsNullOrWhiteSpace(smtpSettings.Domain) + ? new NetworkCredential(smtpSettings.UserName, smtpSettings.Password) + : new NetworkCredential(smtpSettings.UserName, smtpSettings.Password, smtpSettings.Domain) + }; + + return client; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private MailMessage BuildMessageWith(string fromAddress, string toAddress, string subject, string body, bool isBodyHtml, string cc = "") + { + var smtpSettings = _emailGatewaySettings.SmtpSettings.GetValue(); + var message = new MailMessage + { + Subject = (subject ?? "").Replace("\r", " ").Replace("\n", " ").RemoveDoubleSpaces(), + Body = isBodyHtml ? body.WrapAsHtmlDocument() : body, + IsBodyHtml = isBodyHtml, + }; + + message.From = string.IsNullOrWhiteSpace(fromAddress) ? new MailAddress(smtpSettings.DefaultFromAddress) : new MailAddress(fromAddress); + + string[] tos = toAddress.Split(';'); + + foreach (string to in tos) + { + message.To.Add(new MailAddress(to)); + } + + // Add "carbon copies" to email if defined + if (!string.IsNullOrEmpty(cc)) + { + string[] copies = cc.Split(','); + foreach (var copyAddress in copies) + { + message.CC.Add(new MailAddress(copyAddress.Trim())); + } + } + + return message; + } + + #endregion + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs new file mode 100644 index 0000000000..8ad2d61ec3 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Helpers +{ + public static class MobileHelper + { + public static string CleanupMobileNo(string mobileNumber) + { + // Removing any spaces and any other common characters in a phone number. + mobileNumber = mobileNumber.Replace(" ", ""); + mobileNumber = mobileNumber.Replace("-", ""); + mobileNumber = mobileNumber.Replace("(", ""); + mobileNumber = mobileNumber.Replace(")", ""); + + if (mobileNumber.Length < 10) + mobileNumber = mobileNumber.PadLeft(10, '0'); + + // Converting to the required format i.e. '27XXXXXXXXX' + if (mobileNumber.StartsWith("0027")) + mobileNumber = "27" + mobileNumber.Substring(4); + + if (mobileNumber.StartsWith("0")) + mobileNumber = "27" + mobileNumber.Substring(1); + + return mobileNumber; + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs new file mode 100644 index 0000000000..e61f1bac33 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Shesha.OmoNotifications.Helpers +{ + public static class TemplateHelper + { + public static string ReplacePlaceholders(string template, TData data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + // Use regex to find placeholders in the form {{propertyName}} + return Regex.Replace(template, @"\{\{(\w+)\}\}", match => + { + var propertyName = match.Groups[1].Value; + + // Use the runtime type of the object for property lookup + var propertyInfo = data.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(prop => string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase)); + + if (propertyInfo == null) + throw new ArgumentException($"Property '{propertyName}' not found on {data.GetType().Name}"); + + var value = propertyInfo.GetValue(data)?.ToString() ?? string.Empty; + return value; + }); + } + } +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs b/shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs new file mode 100644 index 0000000000..2b49ac864f --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs @@ -0,0 +1,21 @@ +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.Email.Dtos; +using Shesha.Notifications.Dto; +using Shesha.OmoNotifications.Configuration.Email; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications +{ + public interface INotificationChannelSender + { + string GetRecipientId(Person person); + Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc, bool throwException = false, List attachments = null); + Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null); + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs b/shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs new file mode 100644 index 0000000000..7818f1383c --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs @@ -0,0 +1,15 @@ +using Shesha.Domain; +using Shesha.Notifications.Dto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications +{ + public interface INotificationSender + { + Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml); + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs new file mode 100644 index 0000000000..5ad0870792 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs @@ -0,0 +1,147 @@ +using Abp.BackgroundJobs; +using Abp.Dependency; +using Abp.Domain.Repositories; +using Abp.Domain.Uow; +using Abp.Notifications; +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.DynamicEntities.Dtos; +using Shesha.Email.Dtos; +using Shesha.NHibernate; +using Shesha.Notifications.Dto; +using Shesha.OmoNotifications.Dto; +using Shesha.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Jobs +{ + public class BroadcastNotificationJobQueuer : AsyncBackgroundJob, ITransientDependency + { + private readonly IRepository _notificationTopicRepository; + private readonly IRepository _notificationRepository; + private readonly IRepository _notificationTemplateRepository; + private readonly IRepository _notificationChannelRepository; + private readonly IRepository _userTopicSubscriptionRepository; + private readonly IRepository _notificationMessage; + private readonly IRepository _attachmentRepository; + private readonly IEnumerable _channelSenders; + private readonly IRepository _storedFileRepository; + private readonly IStoredFileService _fileService; + private readonly ISessionProvider _sessionProvider; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + + public BroadcastNotificationJobQueuer(IRepository notificationTopicRepository, + IRepository notificationRepository, + IRepository notificationChannelRepository, + IRepository storedFileRepository, + IRepository attachmentRepository, + IRepository notificationTemplateRepository, + IRepository userTopicSubscriptionRepository, + IRepository notificationMessage, + IEnumerable channelSenders, + IStoredFileService fileService, + ISessionProvider sessionProvider, + IUnitOfWorkManager unitOfWorkManager) + { + _notificationMessage = notificationMessage; + _notificationTopicRepository = notificationTopicRepository; + _channelSenders = channelSenders; + _notificationRepository = notificationRepository; + _userTopicSubscriptionRepository = userTopicSubscriptionRepository; + _notificationTemplateRepository = notificationTemplateRepository; + _attachmentRepository = attachmentRepository; + _storedFileRepository = storedFileRepository; + _notificationChannelRepository = notificationChannelRepository; + _fileService = fileService; + _sessionProvider = sessionProvider; + _unitOfWorkManager = unitOfWorkManager; + } + + /// + /// + /// + /// + /// + /// + [UnitOfWork] + public override async Task ExecuteAsync(BroadcastNotificationJobArgs args) + { + var notification = await _notificationRepository.GetAsync(args.NotificationId); + + var template = await _notificationTemplateRepository.GetAsync(args.TemplateId); + + var channel = await _notificationChannelRepository.GetAsync(args.ChannelId); + + var message = await SaveUserNotificationMessagesAsync(notification, template, channel, args.Subject, args.Message, args.Attachments); + + var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == channel.SenderTypeName); + if (senderChannelInterface == null) + throw new Exception($"No sender found for sender type: {channel.SenderTypeName}"); + var sender = new NotificationSender(senderChannelInterface); + await sender.SendBroadcastAsync(notification, message.Subject, message.Message, message.Attachments); + } + + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task SaveUserNotificationMessagesAsync(OmoNotification notification, MessageTemplate template, NotificationChannelConfig channel, string subject, string message, List attachments) + { + var users = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == notification.NotificationTopic.Id); + var notificationMessageDto = new NotificationMessageDto() + { + Message = message, + Subject = subject + }; + + foreach (var user in users) + { + var notificationMessage = new OmoNotificationMessage + { + Subject = subject, + Message = message, + ReadStatus = RefListNotificationReadStatus.Unread, + Direction = RefListNotificationDirection.Outgoing, + Status = RefListNotificationStatus.Preparing, + Channel = channel + }; + + await _notificationMessage.InsertAsync(notificationMessage); + await CurrentUnitOfWork.SaveChangesAsync(); + + if (attachments != null) + { + foreach (var attachmentDto in attachments) + { + var file = await _storedFileRepository.GetAsync(attachmentDto.StoredFileId); + var attachment = new OmoNotificationMessageAttachment + { + PartOf = notificationMessage, + File = file, + FileName = attachmentDto.FileName + }; + + await _attachmentRepository.InsertAsync(attachment); + notificationMessageDto.Attachments.Add(new EmailAttachment(attachmentDto.FileName, _fileService.GetStream(file))); + } + } + + await CurrentUnitOfWork.SaveChangesAsync(); + } + return notificationMessageDto; + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs new file mode 100644 index 0000000000..0ccf60bac6 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs @@ -0,0 +1,44 @@ +using Abp.BackgroundJobs; +using Abp.Dependency; +using Abp.Domain.Repositories; +using Abp.Domain.Uow; +using Shesha.Domain; +using Shesha.DynamicEntities.Dtos; +using Shesha.OmoNotifications.Dto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Jobs +{ + public class DirectNotificationJobQueuer : AsyncBackgroundJob, ITransientDependency + { + private readonly IRepository _personRepository; + private readonly IRepository _notificationMessage; + private readonly IEnumerable _channelSenders; + + + public DirectNotificationJobQueuer(IRepository personRepository, IRepository notificationMessage, IEnumerable channelSenders) + { + _notificationMessage = notificationMessage; + _personRepository = personRepository; + _channelSenders = channelSenders; + + } + + [UnitOfWork] + public override async Task ExecuteAsync(DirectNotificationJobArgs args) + { + var fromPerson = await _personRepository.GetAsync(args.FromPerson); + var toPerson = await _personRepository.GetAsync(args.ToPerson); + var notificationMessage = await _notificationMessage.GetAsync(args.Message); + var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == args.SenderTypeName); + if (senderChannelInterface == null) + throw new Exception($"No sender found for sender type: {args.SenderTypeName}"); + var sender = new NotificationSender(senderChannelInterface); + await sender.SendAsync(fromPerson, toPerson, notificationMessage, true); + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs b/shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs new file mode 100644 index 0000000000..7369f77f08 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs @@ -0,0 +1,188 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Abp.Domain.Uow; +using DocumentFormat.OpenXml.Spreadsheet; +using DocumentFormat.OpenXml.Wordprocessing; +using Hangfire; +using NHibernate.Linq; +using Shesha.Configuration; +using Shesha.Configuration.Email; +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.Email.Dtos; +using Shesha.NHibernate; +using Shesha.Notifications.Dto; +using Shesha.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications +{ + public class NotificationSender: INotificationSender + { + private readonly INotificationChannelSender _channelSender; + + public readonly int MaxRetries = 3; + + public NotificationSender(INotificationChannelSender channelSender) + { + _channelSender = channelSender; + } + + private async Task> GetAttachmentsAsync(OmoNotificationMessage message) + { + var _attachmentRepository = StaticContext.IocManager.Resolve>(); + var _fileService = StaticContext.IocManager.Resolve(); + + var attachments = await _attachmentRepository.GetAll().Where(a => a.PartOf.Id == message.Id).ToListAsync(); + + var result = attachments.Select(a => new EmailAttachment(a.FileName, _fileService.GetStream(a.File))).ToList(); + + return result; + } + + [UnitOfWork] + public async Task SendBroadcastAsync(OmoNotification notification, string subject, string message, List attachments) + { + //int attempt = 0; + //bool sentSuccessfully = false; + //var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); + + await _channelSender.BroadcastAsync(notification.NotificationTopic, subject, message, attachments); + + //var messages = _notificationMessageRepository.GetAll().Where(m => m.PartOf.Id == notification.Id).ToList(); + + //foreach (var message in messages) + //{ + // var fromPerson = message.FromPerson; + // var toPerson = message.ToPerson; + + // while (attempt < MaxRetries && !sentSuccessfully) + // { + // try + // { + // // Attempt to send the message + // sentSuccessfully = _channelSender.Send(fromPerson, toPerson, message, true, "", true, await GetAttachmentsAsync(message)); + + // if (sentSuccessfully) + // { + // // Successful send, exit the loop + // message.Status = RefListNotificationStatus.Sent; + + // break; + // } + // else + // { + // Console.WriteLine($"Attempt {attempt + 1} to send notification failed."); + // } + // } + // catch (Exception ex) + // { + // message.ErrorMessage = $"Exception on attempt {attempt + 1} to send notification: {ex.Message}"; + // Console.WriteLine($"Exception on attempt {attempt + 1} to send notification: {ex.Message}"); + // } + + // // Increment retry count and save to the database + // attempt++; + // message.RetryCount = attempt; + // message.ErrorMessage = $"Failed to send notification after {attempt} attempts."; + // message.Status = RefListNotificationStatus.Failed; + + // using (var uow = StaticContext.IocManager.Resolve().Begin()) + // { + // using (var transaction = StaticContext.IocManager.Resolve().Session.BeginTransaction()) + // { + // await _notificationMessageRepository.UpdateAsync(message); + // transaction.Commit(); + // } + // await uow.CompleteAsync(); + // } + + // // Introduce a delay before the next retry attempt + // if (attempt < MaxRetries) + // { + // await Task.Delay(1000); // Backoff delay + // } + // } + + // if (!sentSuccessfully) + // { + // Console.WriteLine($"Failed to send notification after {MaxRetries} attempts."); + // //throw new Exception("Failed to send notification after maximum retry attempts."); + // } + //} + + } + + + [UnitOfWork] + public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml) + { + var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); + var _unitOfWorkManager = StaticContext.IocManager.Resolve(); + var _sessionProvider = StaticContext.IocManager.Resolve(); + var attachments = await GetAttachmentsAsync(message); + int attempt = 0; + bool sentSuccessfully = false; + + while (attempt < MaxRetries && !sentSuccessfully) + { + try + { + // Attempt to send the message + sentSuccessfully =await _channelSender.SendAsync(fromPerson, toPerson, message, isBodyHtml, "", true, attachments); + + if (sentSuccessfully) + { + // Successful send, exit the loop + message.Status = RefListNotificationStatus.Sent; + + break; + } + else + { + Console.WriteLine($"Attempt {attempt + 1} to send notification failed."); + } + } + catch (Exception ex) + { + message.ErrorMessage = $"Exception on attempt {attempt + 1} to send notification: {ex.Message}"; + Console.WriteLine($"Exception on attempt {attempt + 1} to send notification: {ex.Message}"); + } + + // Increment retry count and save to the database + attempt++; + message.RetryCount = attempt; + message.ErrorMessage = $"Failed to send notification after {attempt} attempts."; + message.Status = RefListNotificationStatus.Failed; + + using (var uow = _unitOfWorkManager.Begin()) + { + + using (var transaction = _sessionProvider.Session.BeginTransaction()) + { + await _notificationMessageRepository.UpdateAsync(message); + transaction.Commit(); + } + await uow.CompleteAsync(); + } + + // Introduce a delay before the next retry attempt + if (attempt < MaxRetries) + { + await Task.Delay(1000); // Backoff delay + } + } + + if (!sentSuccessfully) + { + Console.WriteLine($"Failed to send notification after {MaxRetries} attempts."); + //throw new Exception("Failed to send notification after maximum retry attempts."); + } + } + + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs b/shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs new file mode 100644 index 0000000000..fcd8bb307b --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs @@ -0,0 +1,351 @@ +using Abp.Application.Services.Dto; +using Abp.BackgroundJobs; +using Abp.Domain.Entities; +using Abp.Domain.Repositories; +using Abp.Notifications; +using Abp.UI; +using DocumentFormat.OpenXml.Wordprocessing; +using Hangfire; +using Hangfire.Storage; +using Newtonsoft.Json.Linq; +using NHibernate.Linq; +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.DynamicEntities.Dtos; +using Shesha.EntityReferences; +using Shesha.NotificationMessages.Dto; +using Shesha.Notifications.Dto; +using Shesha.OmoNotifications.Configuration; +using Shesha.OmoNotifications.Dto; +using Shesha.OmoNotifications.Helpers; +using Shesha.OmoNotifications.Jobs; +using Shesha.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications +{ + public class OmoNotificationAppService: SheshaAppServiceBase + { + private readonly IEnumerable _channelSenders; + private readonly IRepository _notificationTypeRepository; + private readonly IRepository _notificationChannelRepository; + private readonly IRepository _messageTemplateRepository; + private readonly IRepository _notificationMessageRepository; + private readonly IRepository _userNotificationPreference; + private readonly IRepository _userTopicSubscriptionRepository; + private readonly IRepository _notificationTopicRepository; + private readonly IRepository _storedFileRepository; + private readonly IRepository _personRepository; + private readonly INotificationSettings _notificationSettings; + private readonly IStoredFileService _storedFileService; + private readonly IBackgroundJobManager _backgroundJobManager; + + public OmoNotificationAppService(IEnumerable channelSenders, + IRepository notificationTypeRepository, + IRepository notificationChannelRepository, + IRepository userNotificationPreference, + IRepository messageTemplateRepository, + IRepository personRepository, + IRepository storedFileRepository, + IRepository userTopicSubscriptionRepository, + IStoredFileService storedFileService, + IRepository notificationMessageRepository, + IRepository notificationTopicRepository, + INotificationSettings notificationSettings, + IBackgroundJobManager backgroundJobManager) + + { + _channelSenders = channelSenders; + _notificationTypeRepository = notificationTypeRepository; + _notificationChannelRepository = notificationChannelRepository; + _userNotificationPreference = userNotificationPreference; + _messageTemplateRepository = messageTemplateRepository; + _notificationSettings = notificationSettings; + _personRepository = personRepository; + _storedFileService = storedFileService; + _notificationMessageRepository = notificationMessageRepository; + _userTopicSubscriptionRepository = userTopicSubscriptionRepository; + _backgroundJobManager = backgroundJobManager; + _storedFileRepository = storedFileRepository; + _notificationTopicRepository = notificationTopicRepository; + } + + public class TestData: NotificationData + { + public string subject { get; set; } + public string name { get; set; } + public string body { get; set; } + } + + public async Task>> OmoTest() + { + var type = await _notificationTypeRepository.FirstOrDefaultAsync(x => x.Name == "Warning"); + var fromPerson = await _personRepository.FirstOrDefaultAsync(x => x.FirstName == "System"); + var toPerson = await _personRepository.FirstOrDefaultAsync(x => x.EmailAddress1 == "omolemo.lethuloe@boxfusion.io"); + var channel = await _notificationChannelRepository.FirstOrDefaultAsync(x => x.Name == "Email"); + var getAttachments = await _storedFileService.GetAttachmentsAsync(fromPerson.Id, "Shesha.Domain.Person"); + + var attachments = getAttachments.Select(x => new NotificationAttachmentDto() + { + FileName = x.FileName, + StoredFileId = x.Id, + }).ToList(); + + + var testing = new TestData() + { + name = "Omolemo", + subject = "Test Subject", + body = "Test Body" + }; + var triggeringEntity = new GenericEntityReference(fromPerson); + return await SendNotification(type, fromPerson, toPerson, data: testing, RefListNotificationPriority.High, attachments, triggeringEntity, channel); + } + + public async Task>> OmoBroadcastTest() + { + var type = await _notificationTypeRepository.FirstOrDefaultAsync(x => x.Name == "Warning"); + var topic = await _notificationTopicRepository.FirstOrDefaultAsync(x => x.Name == "Service Requests"); + var getAttachments = await _storedFileService.GetAttachmentsAsync(topic.Id, "Shesha.Core.NotificationTopic"); + + var attachments = getAttachments.Select(x => new NotificationAttachmentDto() + { + FileName = x.FileName, + StoredFileId = x.Id, + }).ToList(); + + + var testing = new TestData() + { + name = "Omolemo", + subject = "Test Subject", + body = "Test Body" + }; + var triggeringEntity = new GenericEntityReference(topic); + return await SendBroadcastNotification(type, topic, data: testing, RefListNotificationPriority.High, attachments, triggeringEntity); + } + + public async Task>> SendBroadcastNotification(NotificationTypeConfig type, NotificationTopic topic, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData: NotificationData + { + var notification = await SaveOrUpdateEntityAsync(null, item => + { + item.NotificationType = type; + item.NotificationTopic = topic; + item.NotificationData = data.ToString(); + item.Priority = (RefListNotificationPriority)priority; + item.TriggeringEntity = triggeringEntity; + }); + + await CurrentUnitOfWork.SaveChangesAsync(); + + + var users = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == topic.Id); + + if (channel != null) + { + var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channel.SupportedFormat == x.MessageFormat); + var subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data); + var message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); + + await _backgroundJobManager.EnqueueAsync(new BroadcastNotificationJobArgs() + { + TemplateId = template.Id, + NotificationId = notification.Id, + ChannelId = channel.Id, + Subject = subject, + Message = message, + Attachments = attachments + }); + } + else + { + var subscriptions = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == topic.Id); + + if (subscriptions != null && subscriptions.Any()) + { + foreach (var user in users) + { + var userChannels = await GetChannelsAsync(type, user.User, priority); + + foreach (var channelConfig in userChannels) + { + var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); + var subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data); + var message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); + + await _backgroundJobManager.EnqueueAsync(new BroadcastNotificationJobArgs() + { + TemplateId = template.Id, + NotificationId = notification.Id, + ChannelId = channelConfig.Id, + Subject = subject, + Message = message, + Attachments = attachments + }); + } + } + + } + } + + return await MapToDynamicDtoListAsync(new List()); + } + + + public async Task>> SendNotification(NotificationTypeConfig type, Person fromPerson, Person toPerson, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData: NotificationData + { + var notification = await SaveOrUpdateEntityAsync(null, item => + { + item.NotificationType = type; + item.FromPerson = fromPerson; + item.ToPerson = toPerson; + item.NotificationData = data.ToString(); + item.TriggeringEntity = triggeringEntity; + item.Priority = (RefListNotificationPriority)priority; + }); + + await CurrentUnitOfWork.SaveChangesAsync(); + + if (channel != null) + { + // Send notification to a specific channel + await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority,channel, attachments); + } + else + { + // Send notification to all determined channels + var channels = await GetChannelsAsync(type, toPerson, (RefListNotificationPriority)priority); + + foreach (var channelConfig in channels) + { + await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority, channelConfig, attachments); + } + } + + // Return the list of channels used for sending notifications as DynamicDto + return await MapToDynamicDtoListAsync(new List()); + } + + private async Task> GetChannelsAsync(NotificationTypeConfig type, Person recipient, RefListNotificationPriority priority) + { + List results = new List(); + + // Step 1: Check User Notification Preferences + var userPreferences = await _userNotificationPreference.GetAllListAsync( + x => x.User.Id == recipient.Id && x.NotificationType.Id == type.Id + ); + + if (userPreferences != null && userPreferences.Any()) + { + // Flatten and return DefaultChannel from user preferences if available + return userPreferences.Select(x => x.DefaultChannel).ToList(); + } + + // Step 2: Check Override Channels in NotificationTypeConfig + if (!string.IsNullOrEmpty(type.OverrideChannels)) + { + try + { + var overrideChannelIds = JsonSerializer.Deserialize(type.OverrideChannels); + + // Fetch channels by IDs if the repository supports it + var channels = await _notificationChannelRepository + .GetAll() + .Where(channel => overrideChannelIds.Contains(channel.Id)) + .ToListAsync(); + + if (channels.Any()) + { + return channels; + } + } + catch (JsonException ex) + { + new UserFriendlyException("Error deserializing override channels", ex); + // Log deserialization error (ex.Message) and continue to fallback + // Optionally handle the error depending on requirements + } + return results; + } + + // Step 3: Fallback - Return default channels based on priority (if applicable) + var notificationSettings = await _notificationSettings.NotificationSettings.GetValueAsync(); + switch (priority) + { + case RefListNotificationPriority.Low: + return notificationSettings.Low; + case RefListNotificationPriority.Medium: + return notificationSettings.Medium; + case RefListNotificationPriority.High: + return notificationSettings.High; + default: + return new List(); + }; + } + + private async Task ProcessAndSendNotificationToChannel(OmoNotification notification, TData data, Person fromPerson, Person toPerson, NotificationTypeConfig type, RefListNotificationPriority priority, NotificationChannelConfig channelConfig, List attachments = null) where TData: NotificationData + { + var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); + var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == channelConfig.SenderTypeName); + + if (senderChannelInterface == null) + throw new UserFriendlyException($"Sender not found for channel {channelConfig.Name}"); + + // Create a new notification message + var message = await SaveOrUpdateEntityAsync(null, item => + { + item.PartOf = notification; + item.Channel = channelConfig; + item.Subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate,data); + item.Message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); + item.RetryCount = 0; + item.ReadStatus = RefListNotificationReadStatus.Unread; + item.Direction = RefListNotificationDirection.Outgoing; + item.Status = RefListNotificationStatus.Preparing; + }); + await CurrentUnitOfWork.SaveChangesAsync(); + + // save attachments if specified + if (attachments != null) + { + foreach (var attachmentDto in attachments) + { + var file = await _storedFileRepository.GetAsync(attachmentDto.StoredFileId); + + await SaveOrUpdateEntityAsync(null, item => + { + item.PartOf = message; + item.File = file; + item.FileName = attachmentDto.FileName; + }); + } + } + + await CurrentUnitOfWork.SaveChangesAsync(); + + var sender = new NotificationSender(senderChannelInterface); + + if (type.IsTimeSensitive) + { + await sender.SendAsync(fromPerson, toPerson, message, true); + } + else + { + await _backgroundJobManager.EnqueueAsync(new DirectNotificationJobArgs() + { + SenderTypeName = channelConfig.SenderTypeName, + FromPerson = fromPerson.Id, + ToPerson = toPerson.Id, + Message = message.Id + }); + } + + await CurrentUnitOfWork.SaveChangesAsync(); + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs new file mode 100644 index 0000000000..8dc0b3aec8 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs @@ -0,0 +1,72 @@ +using Shesha.Domain; +using Shesha.Email.Dtos; +using Shesha.OmoNotifications.Configuration.Email; +using Shesha.OmoNotifications.Configuration; +using Shesha.OmoNotifications.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Shesha.OmoNotifications.Configuration.Sms; + +namespace Shesha.OmoNotifications.Sms.Gateways +{ + public class ClickatellGateway: ISmsGateway + { + private readonly ISmsGatewaySettings _smsGatewaySettings; + private readonly INotificationSettings _notificationSettings; + public ClickatellGateway(ISmsGatewaySettings smsGatewaySettings, INotificationSettings notificationSettings) + { + _notificationSettings = notificationSettings; + _smsGatewaySettings = smsGatewaySettings; + } + + public async Task SendAsync(string toPerson, string message) + { + var settings= _smsGatewaySettings.ClickatellSettings.GetValueAsync().Result; + + // Send SMS API logic here + using var httpClient = new HttpClient(); + + // Build the request URL + var query = HttpUtility.ParseQueryString(string.Empty); + query["api_id"] = settings.ApiId; + query["user"] = settings.Username; + query["password"] = settings.Password; + query["to"] = MobileHelper.CleanupMobileNo(toPerson); + query["text"] = HttpUtility.UrlEncode(message); + + var url = $"https://api.clickatell.com/http/sendmsg?{query}"; + + try + { + // Send the GET request + var response = await httpClient.GetAsync(url); + + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode && responseContent.StartsWith("ID")) + { + Console.WriteLine("SMS sent successfully!"); + return true; + } + + Console.WriteLine($"Failed to send SMS. Response: {responseContent}"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + return false; + } + } + + public Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs new file mode 100644 index 0000000000..0020edb979 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs @@ -0,0 +1,16 @@ +using Shesha.Domain; +using Shesha.Email.Dtos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Sms.Gateways +{ + public interface ISmsGateway + { + Task SendAsync(string fromPerson, string message); + Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs new file mode 100644 index 0000000000..a5030c2aa0 --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Sms.Gateways +{ + public class SmsGatewayFactory + { + private readonly IServiceProvider _serviceProvider; + + public SmsGatewayFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public ISmsGateway GetGateway(string gatewayName) + { + return gatewayName switch + { + "ClickatellGateway" => _serviceProvider.GetService(), + // Add other gateways here + _ => throw new NotSupportedException($"Gateway {gatewayName} is not supported.") + }; + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs new file mode 100644 index 0000000000..1c576825bb --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs @@ -0,0 +1,80 @@ +using Abp.Domain.Repositories; +using Castle.Core.Logging; +using DocumentFormat.OpenXml.Wordprocessing; +using Newtonsoft.Json; +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.Email.Dtos; +using Shesha.Notifications.Dto; +using Shesha.OmoNotifications.Configuration; +using Shesha.OmoNotifications.Configuration.Email; +using Shesha.OmoNotifications.Configuration.Sms; +using Shesha.OmoNotifications.Emails.Gateways; +using Shesha.OmoNotifications.Helpers; +using Shesha.OmoNotifications.Sms.Gateways; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace Shesha.OmoNotifications.SMS +{ + public class SmsChannelSender : INotificationChannelSender + { + private readonly INotificationSettings _notificationSettings; + private readonly IRepository _notificationGatewayRepository; + private readonly SmsGatewayFactory _smsGatewayFactory; + + public ILogger Logger { get; set; } = NullLogger.Instance; + + public SmsChannelSender(INotificationSettings notificationSettings, SmsGatewayFactory smsGatewayFactory, IRepository notificationGatewayRepository) + { + _notificationSettings = notificationSettings; + _smsGatewayFactory = smsGatewayFactory; + _notificationGatewayRepository = notificationGatewayRepository; + } + + /// + /// + /// + /// + /// + public string GetRecipientId(Person person) + { + return person.MobileNumber1; + } + + /// + /// + /// + /// + private async Task GetSettings() + { + return await _notificationSettings.SmsSettings.GetValueAsync(); + } + + public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + { + var settings = await GetSettings(); + + if (!settings.SmsEnabled) + { + Logger.Warn("SMSs are disabled"); + return await Task.FromResult(false); + } + var preferredGateway = await _notificationGatewayRepository.GetAsync(settings.PreferredGateway.Id); + + var gateway = _smsGatewayFactory.GetGateway(preferredGateway.GatewayTypeName); + return await gateway.SendAsync(GetRecipientId(toPerson), message.Message); + } + + public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) + { + return await Task.FromResult(false); + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs b/shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs new file mode 100644 index 0000000000..1f90b0d9da --- /dev/null +++ b/shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs @@ -0,0 +1,34 @@ +using Shesha.Domain; +using Shesha.Domain.Enums; +using Shesha.Email.Dtos; +using Shesha.Notifications.Dto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.OmoNotifications.Teams +{ + public class TeamsChannelSender : INotificationChannelSender + { + public string GetRecipientId(Person person) + { + // Logic to get Teams ID or use email + return person.EmailAddress1; + } + + public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + { + // Call Teams API to send message + return await Task.FromResult(true); // Replace with actual API call + } + + public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) + { + return await Task.FromResult(false); + } + } + +} diff --git a/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs b/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs index 80147a0550..f50e298bb5 100644 --- a/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs +++ b/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs @@ -11,11 +11,17 @@ using Abp.Reflection.Extensions; using Castle.MicroKernel.Registration; using Shesha.Authorization; +using Shesha.Domain; using Shesha.DynamicEntities; using Shesha.Email; using Shesha.GraphQL; using Shesha.Modules; using Shesha.Notifications; +using Shesha.OmoNotifications; +using Shesha.OmoNotifications.Configuration; +using Shesha.OmoNotifications.Configuration.Email; +using Shesha.OmoNotifications.Configuration.Email.Gateways; +using Shesha.OmoNotifications.Configuration.Sms; using Shesha.Otp; using Shesha.Otp.Configuration; using Shesha.Reflection; @@ -23,6 +29,7 @@ using Shesha.Sms; using Shesha.Sms.Configuration; using Shesha.Startup; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -36,6 +43,8 @@ namespace Shesha typeof(AbpAutoMapperModule))] public class SheshaApplicationModule : SheshaSubModule { + public const int DefaultSingleMessageMaxLength = 160; + public const int DefaultMessagePartLength = 153; public override async Task InitializeConfigurationAsync() { return await ImportConfigurationAsync(); @@ -47,6 +56,7 @@ public override void PreInitialize() Configuration.Auditing.IsEnabled = false; IocManager.Register(); + IocManager.Register(); Configuration.Notifications.Providers.Add(); Configuration.Notifications.Notifiers.Add(); @@ -69,15 +79,58 @@ public override void PreInitialize() Component.For(typeof(IEntityReorderer<,,>)).ImplementedBy(typeof(EntityReorderer<,,>)).LifestyleTransient() ); + #region Notification Settings + + IocManager.RegisterSettingAccessor(s => { + s.NotificationSettings.WithDefaultValue(new NotificationSettings + { + Low = new List { }, + Medium = new List { }, + High = new List { }, + }); + s.SmsSettings.WithDefaultValue(new Shesha.OmoNotifications.Configuration.Sms.SmsSettings + { + SmsEnabled = true, + PreferredGateway = null + }); + s.EmailSettings.WithDefaultValue(new Shesha.OmoNotifications.Configuration.Email.EmailSettings + { + EmailsEnabled = true, + PreferredGateway = null + }); + }); + + IocManager.RegisterSettingAccessor(s => { + s.SmtpSettings.WithDefaultValue(new SmtpSettings + { + Port = 25, + UseSmtpRelay = false, + EnableSsl = false, + }); + }); + + IocManager.RegisterSettingAccessor(s => { + s.ClickatellSettings.WithDefaultValue(new OmoNotifications.Configuration.Sms.Gateways.ClickatellSettings + { + Host = "api.clickatell.com", + SingleMessageMaxLength = DefaultSingleMessageMaxLength, + MessagePartLength = DefaultMessagePartLength + }); + }); + + #endregion + #region SMS Gateways IocManager.RegisterSettingAccessor(s => { - s.SmsSettings.WithDefaultValue(new SmsSettings + s.SmsSettings.WithDefaultValue(new Shesha.Sms.Configuration.SmsSettings { SmsGateway = NullSmsGateway.Uid }); }); + + IocManager.Register(DependencyLifeStyle.Transient); IocManager.IocContainer.Register( diff --git a/shesha-core/src/Shesha.Application/Sms/GatewaySettings.cs b/shesha-core/src/Shesha.Application/Sms/GatewaySettings.cs new file mode 100644 index 0000000000..d3cdfda3fd --- /dev/null +++ b/shesha-core/src/Shesha.Application/Sms/GatewaySettings.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Sms +{ + /// + /// Xml2Sms gateway settings + /// + public class GatewaySettings + { + [Display(Name = "Host")] + public string Host { get; set; } + + [Display(Name = "Api Username")] + public string Username { get; set; } + + [Display(Name = "Api Password")] + public string Password { get; set; } + + /// + /// Use proxy + /// + [Display(Name = "Use proxy")] + public bool UseProxy { get; set; } + + [Display(Name = "Proxy address")] + public string WebProxyAddress { get; set; } + + /// + /// Use default credentials for proxy + /// + [Display(Name = "Use default credentials for proxy")] + public bool UseDefaultProxyCredentials { get; set; } + + [Display(Name = "Proxy username")] + public string WebProxyUsername { get; set; } + + [Display(Name = "Proxy password")] + public string WebProxyPassword { get; set; } + + /// + /// Clickatell Api ID + /// + [Display(Name = "Clickatell Api ID")] + public string ApiId { get; set; } + + /// + /// Max length of single message (default is 160 but may be different in different regions/countries) + /// + [Display(Name = "Max length of single message")] + public int SingleMessageMaxLength { get; set; } + + /// + /// Message part length + /// + [Display(Name = "Message part length", Description = "Is used for multipart messages (153 by default)")] + public int MessagePartLength { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListChannelSupportedMechanism.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListChannelSupportedMechanism.cs new file mode 100644 index 0000000000..c75fabdeeb --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListChannelSupportedMechanism.cs @@ -0,0 +1,18 @@ +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain.Enums +{ + [ReferenceList("Shesha.Core", "ChannelSupportedMechanism")] + public enum RefListChannelSupportedMechanism : long + { + Direct = 1, + BulkSend = 2, + Broadcast = 3 + // Add other formats as needed + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationChannelStatus.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationChannelStatus.cs new file mode 100644 index 0000000000..cf140ee6ac --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationChannelStatus.cs @@ -0,0 +1,17 @@ +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain.Enums +{ + [ReferenceList("Shesha.Core", "NotificationChannelStatus")] + public enum RefListNotificationChannelStatus : long + { + Enabled = 1, + Disabled = 2, + Suppressed = 3, + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationMessageFormat.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationMessageFormat.cs new file mode 100644 index 0000000000..07ac7e6163 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationMessageFormat.cs @@ -0,0 +1,18 @@ +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain.Enums +{ + [ReferenceList("Shesha.Core", "NotificationMessageFormat")] + public enum RefListNotificationMessageFormat : long + { + PlainText = 1, + RichText = 2, + EnhancedText = 3, + // Add other formats as needed + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationPriority.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationPriority.cs new file mode 100644 index 0000000000..8fa73d00f6 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationPriority.cs @@ -0,0 +1,18 @@ +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain.Enums +{ + [ReferenceList("Shesha.Core", "NotificationPriority")] + public enum RefListNotificationPriority : long + { + High = 1, + Medium = 2, + Low = 3, + Deferred = 4 + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs new file mode 100644 index 0000000000..1cef855a40 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/Enums/RefListNotificationReadStatus.cs @@ -0,0 +1,16 @@ +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain.Enums +{ + [ReferenceList("Shesha.Core", "NotificationReadStatus")] + public enum RefListNotificationReadStatus: long + { + Unread = 0, + Read = 1 + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs b/shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs new file mode 100644 index 0000000000..f34e8b54b9 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs @@ -0,0 +1,25 @@ +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.MessageTemplate")] + public class MessageTemplate: FullAuditedEntity + { + [ReferenceList("Shesha.Core", "NotificationMessageFormat")] + public virtual RefListNotificationMessageFormat? MessageFormat { get; set; } + + public virtual NotificationTypeConfig PartOf { get; set; } + [StringLength(2000)] + public virtual string TitleTemplate { get; set; } + [StringLength(int.MaxValue)] + public virtual string BodyTemplate { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs new file mode 100644 index 0000000000..48a4f3f255 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs @@ -0,0 +1,65 @@ +using Shesha.Domain.Attributes; +using Shesha.Domain.ConfigurationItems; +using Shesha.Domain.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [DiscriminatorValue(ItemTypeName)] + [JoinedProperty("Core_NotificationChannelConfigs")] + [Entity(TypeShortAlias = "Shesha.Domain.NotificationChannelConfig")] + public class NotificationChannelConfig : ConfigurationItemBase + { + public NotificationChannelConfig() + { + Init(); + } + + private void Init() + { + VersionStatus = ConfigurationItemVersionStatus.Live; + } + + /// + /// + /// + public const string ItemTypeName = "notification-channel"; + + /// + /// + /// + public override string ItemType => ItemTypeName; + /// + /// + /// + [ReferenceList("Shesha.Core", "NotificationMessageFormat")] + public RefListNotificationMessageFormat? SupportedFormat { get; set; } + /// + /// The maximum supported size for the message in characters + /// + public int MaxMessageSize { get; set; } + /// + /// If true indicates that users may opt out of this notification + /// + [MultiValueReferenceList("Shesha.Core", "ChannelSupportedMechanism")] + public RefListChannelSupportedMechanism? SupportedMechanism { get; set; } + /// + /// The fully qualified name of the class implementing the behavior for this channel through INotificationChannel + /// + public string SenderTypeName { get; set; } + /// + /// The default priority of the message unless overridden during the send operation + /// + [ReferenceList("Shesha.Core", "NotificationPriority")] + public RefListNotificationPriority? DefaultPriority { get; set; } + /// + /// Enabled, Disabled, Suppressed - if suppressed will 'pretend' like the notification will be send, but will simply not send the message + /// + [ReferenceList("Shesha.Core", "NotificationChannelStatus")] + public RefListNotificationChannelStatus? Status { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs new file mode 100644 index 0000000000..9f24e1f113 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs @@ -0,0 +1,47 @@ +using Shesha.Domain.Attributes; +using Shesha.Domain.ConfigurationItems; +using Shesha.Domain.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [DiscriminatorValue(ItemTypeName)] + [JoinedProperty("Core_NotificationGatewayConfigs")] + [Entity(TypeShortAlias = "Shesha.Domain.NotificationGatewayConfig")] + public class NotificationGatewayConfig : ConfigurationItemBase + { + public NotificationGatewayConfig() + { + Init(); + } + + private void Init() + { + VersionStatus = ConfigurationItemVersionStatus.Live; + } + + /// + /// + /// + public const string ItemTypeName = "notification-gateway"; + + /// + /// + /// + public override string ItemType => ItemTypeName; + + /// + /// + /// + public NotificationChannelConfig PartOf { get; set; } + + /// + /// + /// + public string GatewayTypeName { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationTopic.cs b/shesha-core/src/Shesha.Core/Domain/NotificationTopic.cs new file mode 100644 index 0000000000..fc75aa1a8e --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/NotificationTopic.cs @@ -0,0 +1,17 @@ +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.NotificationTopic")] + public class NotificationTopic: FullAuditedEntity + { + public virtual string Name { get; set; } + public virtual string Description { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs new file mode 100644 index 0000000000..e63049dd21 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs @@ -0,0 +1,66 @@ +using Shesha.Domain.Attributes; +using Shesha.Domain.ConfigurationItems; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [DiscriminatorValue(ItemTypeName)] + [JoinedProperty("Core_NotificationTypeConfigs")] + [Entity(TypeShortAlias = "Shesha.Domain.NotificationTypeConfig")] + public class NotificationTypeConfig: ConfigurationItemBase + { + public NotificationTypeConfig() + { + Init(); + } + + private void Init() + { + VersionStatus = ConfigurationItemVersionStatus.Live; + } + + /// + /// + /// + public const string ItemTypeName = "notification-type"; + + /// + /// + /// + public override string ItemType => ItemTypeName; + /// + /// + /// + public bool AllowAttachments { get; set; } + /// + /// + /// + public bool Disable { get; set; } + /// + /// If true indicates that users may opt out of this notification + /// + public bool CanOtpOut { get; set; } + /// + /// + /// + public string Category { get; set; } + /// + /// + /// + public int OrderIndex { get; set; } + /// + /// List of NotificationChannelConfigs + /// + [StringLength(int.MaxValue)] + public string OverrideChannels { get; set; } + /// + /// messages without which the user should not proceed in any case e.g. OTP + /// + public bool IsTimeSensitive { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/OmoNotification.cs b/shesha-core/src/Shesha.Core/Domain/OmoNotification.cs new file mode 100644 index 0000000000..753f2439dd --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/OmoNotification.cs @@ -0,0 +1,52 @@ +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using Shesha.EntityReferences; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.OmoNotification")] + public class OmoNotification: FullAuditedEntity + { + /// + /// + /// + public virtual string Name { get; set; } + /// + /// + /// + public virtual NotificationTypeConfig NotificationType { get; set; } + /// + /// + /// + public virtual Person ToPerson { get; set; } + /// + /// + /// + public virtual Person FromPerson { get; set; } + /// + /// Serialized Json of the notification data + /// + [StringLength(int.MaxValue)] + public virtual string NotificationData { get; set; } + /// + /// + /// + [ReferenceList("Shesha.Core", "NotificationPriority")] + public virtual RefListNotificationPriority Priority { get; set; } + /// + /// The entity that the notification pertains to + /// + public virtual GenericEntityReference TriggeringEntity { get; set; } + /// + /// + /// + public virtual NotificationTopic NotificationTopic { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs b/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs new file mode 100644 index 0000000000..a307bc6d1b --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs @@ -0,0 +1,71 @@ +using Abp.Domain.Entities; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.OmoNotificationMessage")] + public class OmoNotificationMessage : FullAuditedEntityWithExternalSync, IMayHaveTenant + { + /// + /// + /// + public virtual OmoNotification PartOf { get; set; } + /// + /// + /// + public virtual NotificationChannelConfig Channel { get; set; } + /// + /// + /// + public virtual string RecipientText { get; set; } + /// + /// + /// + public virtual string Subject { get; set; } + /// + /// + /// + public virtual string Message { get; set; } + /// + /// Number of attempt to send the message + /// + public virtual int RetryCount { get; set; } + /// + /// Direction (outgoing/incoming) + /// + [ReferenceList("Shesha.Core", "NotificationDirection")] + public virtual RefListNotificationDirection? Direction { get; set; } + /// + /// + /// + [ReferenceList("Shesha.Core", "NotificationReadStatus")] + public virtual RefListNotificationReadStatus? ReadStatus { get; set; } + /// + /// + /// + public virtual DateTime? FirstDateRead { get; set; } + /// + /// + /// + public virtual DateTime? DateSent { get; set; } + /// + /// + /// + public virtual string ErrorMessage { get; set; } + /// + /// + /// + public virtual int? TenantId { get; set; } + /// + /// Status (outgoing/sent/failed etc) + /// + [ReferenceList("Shesha.Core", "NotificationStatus")] + public virtual RefListNotificationStatus Status { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs b/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs new file mode 100644 index 0000000000..1ba80b5a62 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Abp.Domain.Entities.Auditing; + +namespace Shesha.Domain +{ + public class OmoNotificationMessageAttachment : FullAuditedEntity + { + /// + /// Name of the file, is used for overriding of the File's name + /// + [StringLength(300)] + public virtual string FileName { get; set; } + + /// + /// Stored file + /// + public virtual StoredFile File { get; set; } + + /// + /// Stored file + /// + public virtual OmoNotificationMessage PartOf { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/UserNotificationPreference.cs b/shesha-core/src/Shesha.Core/Domain/UserNotificationPreference.cs new file mode 100644 index 0000000000..aec15a9c26 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/UserNotificationPreference.cs @@ -0,0 +1,19 @@ +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.UserNotificationPreference")] + public class UserNotificationPreference: FullAuditedEntity + { + public virtual Person User { get; set; } + public virtual NotificationTypeConfig NotificationType { get; set; } + public virtual bool OptOut { get; set; } + public virtual NotificationChannelConfig DefaultChannel { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/UserTopicSubscription.cs b/shesha-core/src/Shesha.Core/Domain/UserTopicSubscription.cs new file mode 100644 index 0000000000..57e474bf8f --- /dev/null +++ b/shesha-core/src/Shesha.Core/Domain/UserTopicSubscription.cs @@ -0,0 +1,17 @@ +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.UserTopicSubscription")] + public class UserTopicSubscription: FullAuditedEntity + { + public virtual Person User { get; set; } + public virtual NotificationTopic Topic { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs b/shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs new file mode 100644 index 0000000000..78cedd7a0a --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs @@ -0,0 +1,121 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241106152600)] + public class M20241106152600 : Migration + { + public override void Up() + { + Create.Table("Core_NotificationTypeConfigs") + .WithIdAsGuid() + .WithColumn("Core_AllowAttachments").AsBoolean().WithDefaultValue(false) + .WithColumn("Core_Disable").AsBoolean().WithDefaultValue(false) + .WithColumn("Core_CanOtpOut").AsBoolean().WithDefaultValue(false) + .WithColumn("Core_Category").AsString(255).Nullable() + .WithColumn("Core_OrderIndex").AsInt32().Nullable() + .WithColumn("Core_OverrideChannels").AsStringMax().Nullable(); + + Create.ForeignKey("FK_Core_NotificationTypeConfigs_Frwk_ConfigurationItems_Id") + .FromTable("Core_NotificationTypeConfigs") + .ForeignColumn("Id") + .ToTable("Frwk_ConfigurationItems") + .PrimaryColumn("Id"); + + Create.Table("Core_NotificationChannelConfigs") + .WithIdAsGuid() + .WithColumn("Core_SupportedFormatLkp").AsInt64().Nullable() + .WithColumn("Core_MaxMessageSize").AsInt32().Nullable() + .WithColumn("Core_SupportedMechanismLkp").AsInt64().Nullable() + .WithColumn("Core_SenderTypeName").AsString(255).Nullable() + .WithColumn("Core_DefaultPriorityLkp").AsInt64().Nullable() + .WithColumn("Core_StatusLkp").AsInt64().Nullable(); + + Create.ForeignKey("FK_Core_NotificationChannelConfigs_Frwk_ConfigurationItems_Id") + .FromTable("Core_NotificationChannelConfigs") + .ForeignColumn("Id") + .ToTable("Frwk_ConfigurationItems") + .PrimaryColumn("Id"); + + + ///// TODO: Decide between adding new entity or altering the 'NotificationTemplate' entity + Create.Table("Core_MessageTemplates") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithForeignKeyColumn("PartOfId", "Core_NotificationTypeConfigs") + .WithColumn("TitleTemplate").AsString(2000).Nullable() + .WithColumn("BodyTemplate").AsString(int.MaxValue).Nullable() + .WithColumn("MessageFormatLkp").AsInt64().Nullable(); + + + Create.Table("Core_OmoNotifications") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithColumn("Name").AsString().Nullable() + .WithForeignKeyColumn("NotificationTypeId", "Core_NotificationTypeConfigs") + .WithForeignKeyColumn("ToPersonId", "Core_Persons") + .WithForeignKeyColumn("FromPersonId", "Core_Persons") + .WithColumn("NotificationData").AsString(int.MaxValue).Nullable() + .WithColumn("PriorityLkp").AsInt64().Nullable() + .WithColumn("TriggeringEntityId").AsString(100).Nullable() + .WithColumn("TriggeringEntityClassName").AsString(1000).Nullable() + .WithColumn("TriggeringEntityDisplayName").AsString(1000).Nullable(); + + Create.Table("Core_OmoNotificationMessages") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithForeignKeyColumn("PartOfId", "Core_OmoNotifications") + .WithForeignKeyColumn("ChannelId", "Core_NotificationChannelConfigs") + .WithColumn("RecipientText").AsString(int.MaxValue).Nullable() + .WithColumn("Subject").AsString(1000).Nullable() + .WithColumn("Message").AsString(int.MaxValue).Nullable() + .WithColumn("RetryCount").AsInt32().WithDefaultValue(0) + .WithColumn("DirectionLkp").AsInt64().Nullable() + .WithColumn("ReadStatusLkp").AsInt64().Nullable() + .WithColumn("FirstDateRead").AsDateTime().Nullable() + .WithColumn("DateSent").AsDateTime().Nullable() + .WithColumn("ErrorMessage").AsString(int.MaxValue).Nullable() + .WithColumn(DatabaseConsts.ExtSysFirstSyncDate).AsDateTime().Nullable() + .WithColumn(DatabaseConsts.ExtSysId).AsString(50).Nullable() + .WithColumn(DatabaseConsts.ExtSysLastSyncDate).AsDateTime().Nullable() + .WithColumn(DatabaseConsts.ExtSysSource).AsString(50).Nullable() + .WithColumn(DatabaseConsts.ExtSysSyncError).AsStringMax().Nullable() + .WithColumn(DatabaseConsts.ExtSysSyncStatusLkp).AsInt32().Nullable(); + + Alter.Table("Core_OmoNotificationMessages") + .AddTenantIdColumnAsNullable(); + + Create.Table("Core_UserNotificationPreferences") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithForeignKeyColumn("UserId", "Core_Persons") + .WithForeignKeyColumn("NotificationTypeId", "Core_NotificationTypeConfigs") + .WithColumn("OptOut").AsBoolean().WithDefaultValue(false) + .WithForeignKeyColumn("DefaultChannelId", "Core_NotificationChannelConfigs"); + + Create.Table("Core_NotificationTopics") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithColumn("Name").AsString(255).NotNullable() + .WithColumn("Description").AsString(int.MaxValue).Nullable(); + + Create.Table("Core_UserTopicSubscriptions") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithForeignKeyColumn("TopicId", "Core_NotificationTopics") + .WithForeignKeyColumn("UserId", "Core_Persons"); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs b/shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs new file mode 100644 index 0000000000..38d8068c33 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs @@ -0,0 +1,26 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241111155000)] + public class M20241111155000 : Migration + { + public override void Up() + { + Alter.Table("Core_NotificationTypeConfigs") + .AddColumn("Core_IsTimeSensitive").AsBoolean().WithDefaultValue(false); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs b/shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs new file mode 100644 index 0000000000..9b83a2e416 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs @@ -0,0 +1,26 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241111160500)] + public class M20241111160500 : Migration + { + public override void Up() + { + Alter.Table("Core_OmoNotificationMessages") + .AddColumn("StatusLkp").AsInt64().Nullable(); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs b/shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs new file mode 100644 index 0000000000..f79e4020fd --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs @@ -0,0 +1,30 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241114141100)] + public class M20241114141100 : Migration + { + public override void Up() + { + Create.Table("Core_OmoNotificationMessageAttachments") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithColumn("FileName").AsString(300).Nullable() + .WithForeignKeyColumn("PartOfId", "Core_NotificationMessages") + .WithForeignKeyColumn("FileId", "Frwk_StoredFiles"); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs b/shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs new file mode 100644 index 0000000000..f70ae4418c --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs @@ -0,0 +1,31 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241114143900)] + public class M20241114143900 : Migration + { + public override void Up() + { + Delete.ForeignKey("FK_Core_OmoNotificationMessageAttachments_PartOfId_Core_NotificationMessages_Id").OnTable("Core_OmoNotificationMessageAttachments"); + Delete.Index("IX_Core_OmoNotificationMessageAttachments_PartOfId").OnTable("Core_OmoNotificationMessageAttachments"); + Delete.Column("PartOfId").FromTable("Core_OmoNotificationMessageAttachments"); + + + Alter.Table("Core_OmoNotificationMessageAttachments") + .AddForeignKeyColumn("PartOfId", "Core_OmoNotificationMessages"); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs b/shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs new file mode 100644 index 0000000000..9fc0bfb85c --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs @@ -0,0 +1,26 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241114153500)] + public class M20241114153500 : Migration + { + public override void Up() + { + Alter.Table("Core_OmoNotifications") + .AddForeignKeyColumn("NotificationTopicId", "Core_NotificationTopics"); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs b/shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs new file mode 100644 index 0000000000..31afa03015 --- /dev/null +++ b/shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs @@ -0,0 +1,34 @@ +using FluentMigrator; +using NUglify; +using Shesha.FluentMigrator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Migrations +{ + [Migration(20241121180900)] + public class M20241121180900 : Migration + { + public override void Up() + { + Create.Table("Core_NotificationGatewayConfigs") + .WithIdAsGuid() + .WithForeignKeyColumn("Core_PartOfId", "Core_NotificationChannelConfigs") + .WithColumn("Core_GatewayTypeName").AsString(255).Nullable(); + + Create.ForeignKey("FK_Core_NotificationGatewayConfigs_Frwk_ConfigurationItems_Id") + .FromTable("Core_NotificationGatewayConfigs") + .ForeignColumn("Id") + .ToTable("Frwk_ConfigurationItems") + .PrimaryColumn("Id"); + } + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs b/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs index 4a186b5cc1..8ebebb081a 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs @@ -12,10 +12,14 @@ public static class SheshaSettingNames public const string SmsSettings = "Shesha.SmsSettings"; + public const string OmoSmsSettings = "Shesha.OmoSmsSettings"; + public const string ThemeSettings = "Shesha.ThemeSettings"; public const string MainMenuSettings = "Shesha.MainMenuSettings"; public const string DefaultUrl = "Shesha.DefaultUrl"; + + public const string NotificationSettings = "Shesha.NotificationSettings"; } } diff --git a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs index 03792dc4b5..8e1650f450 100644 --- a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs +++ b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs @@ -31,6 +31,13 @@ using Shesha.GraphQL.Middleware; using Shesha.GraphQL.Swagger; using Shesha.Identity; +using Shesha.OmoNotifications; +using Shesha.OmoNotifications.Configuration.Sms.Gateways; +using Shesha.OmoNotifications.Emails; +using Shesha.OmoNotifications.Emails.Gateways; +using Shesha.OmoNotifications.Sms.Gateways; +using Shesha.OmoNotifications.SMS; +using Shesha.OmoNotifications.Teams; using Shesha.Scheduler.Extensions; using Shesha.Scheduler.Hangfire; using Shesha.Specifications; @@ -96,6 +103,14 @@ public IServiceProvider ConfigureServices(IServiceCollection services) IdentityRegistrar.Register(services); AuthConfigurer.Configure(services, _appConfiguration); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSignalR(); services.AddCors(); diff --git a/shesha-core/src/Shesha.Web.Host/appsettings.json b/shesha-core/src/Shesha.Web.Host/appsettings.json index b1f7eed64a..f1b0e412c3 100644 --- a/shesha-core/src/Shesha.Web.Host/appsettings.json +++ b/shesha-core/src/Shesha.Web.Host/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Default": "Data Source=.; Initial Catalog=function-shesha-test;Integrated Security=SSPI;TrustServerCertificate=True;" + "Default": "Data Source=MSI; Initial Catalog=pd-crm-shesha-testV2;Integrated Security=SSPI;TrustServerCertificate=True;" }, "App": { "ServerRootAddress": "http://sheshacore-demo.boxfusion.co.za", From c029e9fb80290721c07ddb1d6f55f3364583d57a Mon Sep 17 00:00:00 2001 From: Omolemo Lethuloe Date: Wed, 27 Nov 2024 21:38:39 +0200 Subject: [PATCH 2/5] Notifications framework rework - version 1 --- .../package20241125_1033.shaconfig | Bin 0 -> 7366 bytes .../package20241127_1431.shaconfig | Bin 0 -> 3979 bytes .../package20241127_2044.shaconfig | Bin 0 -> 25209 bytes .../package20241127_2131.shaconfig | Bin 0 -> 9742 bytes .../Dto/NotificationMessageDto.cs | 89 -- .../Dto/NotificationTemplateMapProfile.cs | 38 - .../NotificationMessageAppService.cs | 53 +- .../Configuration/Email/EmailSettings.cs | 2 +- .../Email/Gateways/SmtpSettings.cs | 2 +- .../Email/IEmailGatewaySettings.cs | 6 +- .../Configuration/INotificationSettings.cs | 6 +- .../NotificationGatewaySettingNames.cs | 2 +- .../Configuration/NotificationSettingNames.cs | 2 +- .../Configuration/NotificationSettings.cs | 2 +- .../Sms/Gateways/ClickatellSettings.cs | 2 +- .../Configuration/Sms/ISmsGatewaySettings.cs | 4 +- .../Configuration/Sms/SmsSettings.cs | 2 +- .../Dto/DistributedNotificationChannel.cs | 42 + .../INotificationChannelExport.cs | 12 + .../INotificationChannelImport.cs | 12 + .../NotificationChannelExport.cs | 80 ++ .../NotificationChannelImport.cs | 126 +++ .../Dto/DistributedNotificationGateway.cs | 25 + .../INotificationGatewayExport.cs | 12 + .../INotificationGatewayImport.cs | 12 + .../NotificationGatewayExport.cs | 76 ++ .../NotificationGatewayImport.cs | 126 +++ .../Dto/DistributedNotificationType.cs | 40 + .../INotificationTypeExport.cs | 12 + .../INotificationTypeImport.cs | 12 + .../NotificationTypeExport.cs | 79 ++ .../NotificationTypeImport.cs | 126 +++ .../Dto/BroadcastNotificationJobArgs.cs | 2 +- .../Dto/DirectNotificationJobArgs.cs | 3 +- .../Dto/NotificationMessageDto.cs | 2 +- .../Notifications/Dto/ShaNotificationData.cs | 106 --- .../Notifications/Dto/TestNotificationData.cs | 11 - .../Notifications/EmailRealTimeNotifier.cs | 137 ---- .../Emails/EmailChannelSender.cs | 11 +- .../Emails/Gateways/EmailGatewayFactory.cs | 2 +- .../Emails/Gateways/IEmailGateway.cs | 4 +- .../Emails/Gateways/SmtpGateway.cs | 10 +- ...ShaNotificationFailedWaitRetryException.cs | 17 - ...haNotificationIsStillPreparingException.cs | 17 - .../ShaNotificationNotFoundException.cs | 16 - .../ShaNotificationSaveFailedException.cs | 20 - .../Helpers/MobileHelper.cs | 2 +- .../Helpers/TemplateHelper.cs | 2 +- .../Notifications/INotificationAppService.cs | 127 --- .../INotificationChannelSender.cs | 7 +- .../INotificationSender.cs | 5 +- .../IShaNotificationDistributer.cs | 18 - .../Notifications/IShaRealTimeNotifier.cs | 30 - .../Jobs/BroadcastNotificationJobQueuer.cs | 27 +- .../Jobs/DirectNotificationJobQueuer.cs | 8 +- .../Notifications/NotificationAppService.cs | 767 +++++++----------- .../NotificationPublicationContext.cs | 78 -- .../NotificationSender.cs | 158 ++-- .../Notifications/RealTimeNotifierBase.cs | 557 ------------- .../ShaNotificationDistributer.cs | 367 --------- .../Notifications/ShaNotificationProvider.cs | 23 - .../Notifications/ShaNotificationPublisher.cs | 142 ---- .../Sms/Gateways/ClickatellGateway.cs | 10 +- .../Sms/Gateways/ISmsGateway.cs | 2 +- .../Sms/Gateways/SmsGatewayFactory.cs | 2 +- .../Sms/SmsChannelSender.cs | 17 +- .../Notifications/SmsRealTimeNotifier.cs | 105 --- .../Teams/TeamsChannelSender.cs | 5 +- .../OmoNotificationAppService.cs | 351 -------- .../Shesha.Application.csproj | 8 + .../SheshaApplicationModule.cs | 74 +- .../src/Shesha.Core/Domain/MessageTemplate.cs | 25 - .../src/Shesha.Core/Domain/Notification.cs | 100 ++- .../Shesha.Core/Domain/NotificationMessage.cs | 108 +-- .../Domain/NotificationTemplate.cs | 79 +- .../src/Shesha.Core/Domain/OmoNotification.cs | 52 -- .../Domain/OmoNotificationMessage.cs | 71 -- .../OmoNotificationMessageAttachment.cs | 25 - .../Shesha.Core/Migrations/M20241111155000.cs | 26 - .../Shesha.Core/Migrations/M20241111160500.cs | 26 - .../Shesha.Core/Migrations/M20241114141100.cs | 30 - .../Shesha.Core/Migrations/M20241114143900.cs | 31 - .../Shesha.Core/Migrations/M20241114153500.cs | 26 - .../Shesha.Core/Migrations/M20241121180900.cs | 34 - ...{M20241106152600.cs => M20241127170300.cs} | 136 +++- .../src/Shesha.Web.Host/Startup/Startup.cs | 12 +- 86 files changed, 1530 insertions(+), 3503 deletions(-) create mode 100644 shesha-core/src/Shesha.Application/ConfigMigrations/package20241125_1033.shaconfig create mode 100644 shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_1431.shaconfig create mode 100644 shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_2044.shaconfig create mode 100644 shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_2131.shaconfig delete mode 100644 shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationMessageDto.cs delete mode 100644 shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationTemplateMapProfile.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/Email/EmailSettings.cs (92%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/Email/Gateways/SmtpSettings.cs (96%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/Email/IEmailGatewaySettings.cs (78%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/INotificationSettings.cs (87%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/NotificationGatewaySettingNames.cs (89%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/NotificationSettingNames.cs (89%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/NotificationSettings.cs (94%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/Sms/Gateways/ClickatellSettings.cs (96%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/Sms/ISmsGatewaySettings.cs (86%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Configuration/Sms/SmsSettings.cs (93%) create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelExport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelImport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelExport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/Dto/DistributedNotificationGateway.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayExport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayImport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayExport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayImport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeExport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeImport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Dto/BroadcastNotificationJobArgs.cs (94%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Dto/DirectNotificationJobArgs.cs (85%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Dto/NotificationMessageDto.cs (90%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Dto/ShaNotificationData.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Dto/TestNotificationData.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/EmailRealTimeNotifier.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Emails/EmailChannelSender.cs (91%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Emails/Gateways/EmailGatewayFactory.cs (94%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Emails/Gateways/IEmailGateway.cs (68%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Emails/Gateways/SmtpGateway.cs (94%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationFailedWaitRetryException.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationIsStillPreparingException.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationNotFoundException.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationSaveFailedException.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Helpers/MobileHelper.cs (96%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Helpers/TemplateHelper.cs (96%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/INotificationAppService.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/INotificationChannelSender.cs (67%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/INotificationSender.cs (53%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/IShaNotificationDistributer.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/IShaRealTimeNotifier.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Jobs/BroadcastNotificationJobQueuer.cs (82%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Jobs/DirectNotificationJobQueuer.cs (83%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/NotificationPublicationContext.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/NotificationSender.cs (52%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/RealTimeNotifierBase.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/ShaNotificationDistributer.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/ShaNotificationProvider.cs delete mode 100644 shesha-core/src/Shesha.Application/Notifications/ShaNotificationPublisher.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Sms/Gateways/ClickatellGateway.cs (90%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Sms/Gateways/ISmsGateway.cs (89%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Sms/Gateways/SmsGatewayFactory.cs (94%) rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Sms/SmsChannelSender.cs (83%) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/SmsRealTimeNotifier.cs rename shesha-core/src/Shesha.Application/{OmoNotifications => Notifications}/Teams/TeamsChannelSender.cs (80%) delete mode 100644 shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs delete mode 100644 shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs delete mode 100644 shesha-core/src/Shesha.Core/Domain/OmoNotification.cs delete mode 100644 shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs delete mode 100644 shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs delete mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs delete mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs delete mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs delete mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs delete mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs delete mode 100644 shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs rename shesha-core/src/Shesha.Core/Migrations/{M20241106152600.cs => M20241127170300.cs} (52%) diff --git a/shesha-core/src/Shesha.Application/ConfigMigrations/package20241125_1033.shaconfig b/shesha-core/src/Shesha.Application/ConfigMigrations/package20241125_1033.shaconfig new file mode 100644 index 0000000000000000000000000000000000000000..f9c30611e87a87913d0e612606aa9951ab3bb8cd GIT binary patch literal 7366 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 literal 0 HcmV?d00001 diff --git a/shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_1431.shaconfig b/shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_1431.shaconfig new file mode 100644 index 0000000000000000000000000000000000000000..7708796891d0cd54b1d639dc4b1b826e355a2bf4 GIT binary patch literal 3979 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 literal 0 HcmV?d00001 diff --git a/shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_2131.shaconfig b/shesha-core/src/Shesha.Application/ConfigMigrations/package20241127_2131.shaconfig new file mode 100644 index 0000000000000000000000000000000000000000..1c14df99839ab056c3d844ff3e56a4a275297a5a GIT binary patch literal 9742 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 T`;XT!|7;9sD5wMK|2X{*4mP-^ literal 0 HcmV?d00001 diff --git a/shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationMessageDto.cs b/shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationMessageDto.cs deleted file mode 100644 index db971973e8..0000000000 --- a/shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationMessageDto.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Abp.Application.Services.Dto; -using Shesha.AutoMapper.Dto; -using Shesha.Notifications.Dto; -using System; -using System.Collections.Generic; - -namespace Shesha.NotificationMessages.Dto -{ - public class NotificationMessageDto : EntityDto - { - /// - /// Person who sent this message - /// - public EntityReferenceDto Sender { get; set; } - - /// - /// Recipient (person) of the message - /// - public EntityReferenceDto Recipient { get; set; } - - /// - /// Send type (email/sms/push etc) - /// - public ReferenceListItemValueDto SendType { get; set; } - - /// - /// Recipient text (email address/mobile number etc) - /// - public string RecipientText { get; set; } - - /// - /// Subject of the message - /// - public string Subject { get; set; } - - /// - /// Message body - /// - public string Body { get; set; } - - /// - /// Template that was used for message generation - /// - public EntityReferenceDto Template { get; set; } - - /// - /// Notification - /// - public EntityReferenceDto Notification { get; set; } - - /// - /// Source Entity - /// - public EntityReferenceDto SourceEntity { get; set; } - - /// - /// Attachments - /// - public List Attachments { get; set; } = new List(); - - /// - /// Date and time of last attempt to send the message - /// - public DateTime? SendDate { get; set; } - - /// - /// Number of attempt to send the message - /// - public int TryCount { get; set; } - - /// - /// Status (outgoing/sent/failed etc) - /// - public ReferenceListItemValueDto Status { get; set; } - - /// - /// Error message - /// - public string ErrorMessage { get; set; } - /// - /// Indicates whether or not a user has viewed the notificationMessage, - /// - public bool? Opened { get; set; } - /// - /// The Date and time the user last viewed the notificationMessage - /// - public DateTime? LastOpened { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationTemplateMapProfile.cs b/shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationTemplateMapProfile.cs deleted file mode 100644 index 4f3bcad1f3..0000000000 --- a/shesha-core/src/Shesha.Application/NotificationMessages/Dto/NotificationTemplateMapProfile.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Shesha.AutoMapper; -using Shesha.AutoMapper.Dto; -using Shesha.Domain; -using Shesha.Extensions; -using Shesha.Notifications.Dto; -using Shesha.Utilities; - -namespace Shesha.NotificationMessages.Dto -{ - public class NotificationTemplateMapProfile : ShaProfile - { - public NotificationTemplateMapProfile() - { - CreateMap() - .ForMember(u => u.Sender, - options => options.MapFrom(e => e.Sender != null - ? new EntityReferenceDto(e.Sender.Id, e.Sender.FullName, e.Sender.GetClassName()) : null)) - .ForMember(u => u.Recipient, - options => options.MapFrom(e => e.Recipient != null - ? new EntityReferenceDto(e.Recipient.Id, e.Recipient.FullName, e.Recipient.GetClassName()) : null)) - .ForMember(u => u.Template, - options => options.MapFrom(e => e.Template != null - ? new EntityReferenceDto(e.Template.Id, e.Template.Name, e.Template.GetClassName()) : null)) - .ForMember(u => u.Notification, - options => options.MapFrom(e => e.Notification != null - ? new EntityReferenceDto(e.Notification.Id, e.Notification.Name, e.Notification.GetClassName()) : null)) - .ForMember(u => u.SourceEntity, - options => options.MapFrom(e => e.SourceEntity != null - ? new EntityReferenceDto(e.SourceEntity.Id.Replace('"', ' ').Trim().ToGuid(), e.SourceEntity._displayName, e.SourceEntity._className) : null)) - .MapReferenceListValuesToDto(); - - CreateMap() - .ForMember(u => u.StoredFileId, - options => options.MapFrom(e => e.File != null ? e.File.Id : Guid.Empty)); - } - } -} diff --git a/shesha-core/src/Shesha.Application/NotificationMessages/NotificationMessageAppService.cs b/shesha-core/src/Shesha.Application/NotificationMessages/NotificationMessageAppService.cs index af839103ca..653f3380c4 100644 --- a/shesha-core/src/Shesha.Application/NotificationMessages/NotificationMessageAppService.cs +++ b/shesha-core/src/Shesha.Application/NotificationMessages/NotificationMessageAppService.cs @@ -1,51 +1,34 @@ using Abp.Domain.Repositories; using Shesha.Domain; +using Shesha.Domain.Enums; using Shesha.DynamicEntities.Dtos; -using Shesha.NotificationMessages.Dto; -using Shesha.Notifications; using System; using System.Threading.Tasks; -namespace Shesha.NotificationMessages; - -/// -/// Notifications audit service -/// -public class NotificationMessageAppService : SheshaCrudServiceBase +namespace Shesha.NotificationMessages { - private readonly IShaNotificationDistributer _distributer; - - public NotificationMessageAppService(IRepository repository, IShaNotificationDistributer distributer) : base(repository) - { - _distributer = distributer; - } - /// - /// Resend notification message with specified + /// Notifications audit service /// - /// - /// - public async Task Resend(Guid id) + public class NotificationMessageAppService : SheshaCrudServiceBase, Guid> { - var notificationMessage = await Repository.GetAsync(id); - - var dto = ObjectMapper.Map(notificationMessage); - await _distributer.ResendMessageAsync(dto); - return true; - } - - public async Task> MarkAsReadAsync(Guid id) - { - if (id == Guid.Empty) - throw new ArgumentNullException(nameof(id)); + public NotificationMessageAppService(IRepository repository) : base(repository) + { + } - var entity = await SaveOrUpdateEntityAsync(id, item => + public async Task> MarkAsReadAsync(Guid id) { - item.Opened = true; - item.LastOpened = DateTime.UtcNow; - }); + if (id == Guid.Empty) + throw new ArgumentNullException(nameof(id)); + + var entity = await SaveOrUpdateEntityAsync(id, (Action)(item => + { + item.ReadStatus = RefListNotificationReadStatus.Read; + item.FirstDateRead = DateTime.UtcNow; + })); - return await MapToDynamicDtoAsync(entity); + return await MapToDynamicDtoAsync(entity); + } } } \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/Email/EmailSettings.cs similarity index 92% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/Email/EmailSettings.cs index 0f5bf1c633..2d85240113 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/EmailSettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/Email/EmailSettings.cs @@ -1,6 +1,6 @@ using Shesha.Domain; -namespace Shesha.OmoNotifications.Configuration.Email +namespace Shesha.Notifications.Configuration.Email { /// /// Email settings diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/Email/Gateways/SmtpSettings.cs similarity index 96% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/Email/Gateways/SmtpSettings.cs index e7ef7d6e7d..7639cf0d0a 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/Gateways/SmtpSettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/Email/Gateways/SmtpSettings.cs @@ -1,4 +1,4 @@ -namespace Shesha.OmoNotifications.Configuration.Email.Gateways +namespace Shesha.Notifications.Configuration.Email.Gateways { /// /// SMTP settings diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/Email/IEmailGatewaySettings.cs similarity index 78% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/Email/IEmailGatewaySettings.cs index 316e34f7e1..cd3cc973bd 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Email/IEmailGatewaySettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/Email/IEmailGatewaySettings.cs @@ -1,5 +1,5 @@ -using Shesha.OmoNotifications.Configuration.Email.Gateways; -using Shesha.OmoNotifications.Configuration.Sms.Gateways; +using Shesha.Notifications.Configuration.Email.Gateways; +using Shesha.Notifications.Configuration.Sms.Gateways; using Shesha.Settings; using Shesha.Sms; using System; @@ -10,7 +10,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Configuration.Email +namespace Shesha.Notifications.Configuration.Email { [Category("Email Gateways")] public interface IEmailGatewaySettings: ISettingAccessors diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/INotificationSettings.cs similarity index 87% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/INotificationSettings.cs index e41d90e2af..05a3fc90e8 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/INotificationSettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/INotificationSettings.cs @@ -1,10 +1,10 @@ -using Shesha.OmoNotifications.Configuration.Email; -using Shesha.OmoNotifications.Configuration.Sms; +using Shesha.Notifications.Configuration.Email; +using Shesha.Notifications.Configuration.Sms; using Shesha.Settings; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -namespace Shesha.OmoNotifications.Configuration +namespace Shesha.Notifications.Configuration { /// /// SMS Settings diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationGatewaySettingNames.cs similarity index 89% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationGatewaySettingNames.cs index d53a58589a..1ed28f1da2 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationGatewaySettingNames.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationGatewaySettingNames.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Configuration +namespace Shesha.Notifications.Configuration { public static class NotificationGatewaySettingNames { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationSettingNames.cs similarity index 89% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationSettingNames.cs index 0d61f55663..683b40c727 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettingNames.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationSettingNames.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Configuration +namespace Shesha.Notifications.Configuration { public static class NotificationSettingNames { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationSettings.cs similarity index 94% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationSettings.cs index 0894362696..797cdf75d1 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/NotificationSettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/NotificationSettings.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Configuration +namespace Shesha.Notifications.Configuration { /// /// Enter preferred channels per priority. diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/Gateways/ClickatellSettings.cs similarity index 96% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/Gateways/ClickatellSettings.cs index f81dc79ffb..2988d838bc 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/Gateways/ClickatellSettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/Gateways/ClickatellSettings.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Shesha.OmoNotifications.Configuration.Sms.Gateways +namespace Shesha.Notifications.Configuration.Sms.Gateways { /// /// Xml2Sms gateway settings diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/ISmsGatewaySettings.cs similarity index 86% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/ISmsGatewaySettings.cs index ccf7dac189..62228ca0b1 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/ISmsGatewaySettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/ISmsGatewaySettings.cs @@ -1,5 +1,5 @@ using Shesha.Configuration; -using Shesha.OmoNotifications.Configuration.Sms.Gateways; +using Shesha.Notifications.Configuration.Sms.Gateways; using Shesha.Settings; using Shesha.Sms; using System; @@ -10,7 +10,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Configuration.Sms +namespace Shesha.Notifications.Configuration.Sms { [Category("SMS Gateways")] public interface ISmsGatewaySettings: ISettingAccessors diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs b/shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/SmsSettings.cs similarity index 93% rename from shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs rename to shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/SmsSettings.cs index 6864db6840..7c47241589 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Configuration/Sms/SmsSettings.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Configuration/Sms/SmsSettings.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Configuration.Sms +namespace Shesha.Notifications.Configuration.Sms { public class SmsSettings { diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs new file mode 100644 index 0000000000..25a5e48596 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/Dto/DistributedNotificationChannel.cs @@ -0,0 +1,42 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using System; + +namespace Shesha.Notifications.Distribution.NotificationChannels.Dto +{ + /// + /// Distributed file template + /// + public class DistributedNotificationChannel: DistributedConfigurableItemBase + { + /// + /// + /// + [ReferenceList("Shesha.Core", "NotificationMessageFormat")] + public RefListNotificationMessageFormat? SupportedFormat { get; set; } + /// + /// The maximum supported size for the message in characters + /// + public int MaxMessageSize { get; set; } + /// + /// If true indicates that users may opt out of this notification + /// + [MultiValueReferenceList("Shesha.Core", "ChannelSupportedMechanism")] + public RefListChannelSupportedMechanism? SupportedMechanism { get; set; } + /// + /// The fully qualified name of the class implementing the behavior for this channel through INotificationChannel + /// + public string SenderTypeName { get; set; } + /// + /// The default priority of the message unless overridden during the send operation + /// + [ReferenceList("Shesha.Core", "NotificationPriority")] + public RefListNotificationPriority? DefaultPriority { get; set; } + /// + /// Enabled, Disabled, Suppressed - if suppressed will 'pretend' like the notification will be send, but will simply not send the message + /// + [ReferenceList("Shesha.Core", "NotificationChannelStatus")] + public RefListNotificationChannelStatus? Status { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelExport.cs new file mode 100644 index 0000000000..77ae359d4b --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelExport.cs @@ -0,0 +1,12 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; + +namespace Shesha.Notifications.Distribution.NotificationChannels +{ + /// + /// file template import + /// + public interface INotificationChannelExport : IConfigurableItemExport + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelImport.cs new file mode 100644 index 0000000000..a9c92cd048 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/INotificationChannelImport.cs @@ -0,0 +1,12 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; + +namespace Shesha.Notifications.Distribution.NotificationChannels +{ + /// + /// file template export + /// + public interface INotificationChannelImport : IConfigurableItemImport + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelExport.cs new file mode 100644 index 0000000000..95c28013e3 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelExport.cs @@ -0,0 +1,80 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Notifications.Distribution.NotificationChannels.Dto; +using Shesha.Services; +using Shesha.StoredFiles; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Shesha.Notifications.Distribution.NotificationChannels +{ + /// + /// file template import + /// + public class NotificationChannelExport : INotificationChannelExport, ITransientDependency + { + private readonly IRepository _configurationRepo; + + public NotificationChannelExport(IRepository configurationRepo) + { + _configurationRepo = configurationRepo; + } + + public string ItemType => NotificationChannelConfig.ItemTypeName; + + public async Task ExportItemAsync(Guid id) + { + var item = await _configurationRepo.GetAsync(id); + return await ExportItemAsync(item); + } + + public async Task ExportItemAsync(ConfigurationItemBase item) + { + if (!(item is NotificationChannelConfig itemConfig)) + throw new ArgumentException($"Wrong type of argument {item}. Expected {nameof(NotificationChannelConfig)}, actual: {item.GetType().FullName}", nameof(item)); + + var result = new DistributedNotificationChannel + { + Id = itemConfig.Id, + Name = itemConfig.Name, + ModuleName = itemConfig.Module?.Name, + FrontEndApplication = itemConfig.Application?.AppKey, + ItemType = itemConfig.ItemType, + + Label = itemConfig.Label, + Description = itemConfig.Description, + OriginId = itemConfig.Origin?.Id, + BaseItem = itemConfig.BaseItem?.Id, + VersionNo = itemConfig.VersionNo, + VersionStatus = itemConfig.VersionStatus, + ParentVersionId = itemConfig.ParentVersion?.Id, + Suppress = itemConfig.Suppress, + + // specific properties + + SupportedFormat = itemConfig.SupportedFormat, + MaxMessageSize = itemConfig.MaxMessageSize, + SupportedMechanism = itemConfig.SupportedMechanism, + SenderTypeName = itemConfig.SenderTypeName, + DefaultPriority = itemConfig.DefaultPriority, + Status = itemConfig.Status, + }; + + return await Task.FromResult(result); + } + + /// inheritedDoc + public async Task WriteToJsonAsync(DistributedConfigurableItemBase item, Stream jsonStream) + { + var json = JsonConvert.SerializeObject(item, Formatting.Indented); + using (var writer = new StreamWriter(jsonStream)) + { + await writer.WriteAsync(json); + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs new file mode 100644 index 0000000000..7de3aa8a45 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationChannels/NotificationChannelImport.cs @@ -0,0 +1,126 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Domain.ConfigurationItems; +using Shesha.Notifications.Distribution.NotificationChannels.Dto; +using Shesha.EntityReferences; +using Shesha.Services; +using Shesha.Services.ConfigurationItems; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Shesha.Notifications.Distribution.NotificationChannels +{ + /// + /// file template import + /// + public class NotificationChannelImport : ConfigurationItemImportBase, INotificationChannelImport, ITransientDependency + { + public readonly IRepository _configurationRepo; + + public NotificationChannelImport( + IRepository moduleRepo, + IRepository frontEndAppRepo, + IRepository configurationRepo + ) : base (moduleRepo, frontEndAppRepo) + { + _configurationRepo = configurationRepo; + } + + public string ItemType => NotificationChannelConfig.ItemTypeName; + + public async Task ImportItemAsync(DistributedConfigurableItemBase item, IConfigurationItemsImportContext context) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (!(item is DistributedNotificationChannel itemConfig)) + throw new NotSupportedException($"{this.GetType().FullName} supports only items of type {nameof(NotificationChannelConfig)}. Actual type is {item.GetType().FullName}"); + + return await ImportAsync(itemConfig, context); + } + + protected async Task ImportAsync(DistributedNotificationChannel item, IConfigurationItemsImportContext context) + { + // use status specified in the context with fallback to imported value + var statusToImport = context.ImportStatusAs ?? item.VersionStatus; + + // get DB config + var dbItem = await _configurationRepo.FirstOrDefaultAsync(x => x.Name == item.Name && x.Description == item.Description + && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) + && x.IsLast); + + if (dbItem != null) + { + + // ToDo: Temporary update the current version. + // Need to update the rest of the other code to work with versioning first + + await MapConfigAsync(item, dbItem, context); + await _configurationRepo.UpdateAsync(dbItem); + } + else + { + dbItem = new NotificationChannelConfig(); + await MapConfigAsync(item, dbItem, context); + + // fill audit? + dbItem.VersionNo = 1; + dbItem.Module = await GetModuleAsync(item.ModuleName, context); + + // important: set status according to the context + dbItem.VersionStatus = statusToImport; + dbItem.CreatedByImport = context.ImportResult; + + dbItem.Normalize(); + await _configurationRepo.InsertAsync(dbItem); + } + return dbItem; + } + + + protected async Task MapConfigAsync(DistributedNotificationChannel item, NotificationChannelConfig dbItem, IConfigurationItemsImportContext context) + { + dbItem.Name = item.Name; + dbItem.Module = await GetModuleAsync(item.ModuleName, context); + dbItem.Application = await GetFrontEndAppAsync(item.FrontEndApplication, context); + dbItem.ItemType = item.ItemType; + + dbItem.Label = item.Label; + dbItem.Description = item.Description; + dbItem.VersionNo = item.VersionNo; + dbItem.VersionStatus = item.VersionStatus; + dbItem.Suppress = item.Suppress; + + // entity specific properties + dbItem.SupportedFormat = item.SupportedFormat; + dbItem.MaxMessageSize = item.MaxMessageSize; + dbItem.SupportedMechanism = item.SupportedMechanism; + dbItem.SenderTypeName = item.SenderTypeName; + dbItem.DefaultPriority = item.DefaultPriority; + dbItem.Status = item.Status; + + return dbItem; + } + + public async Task ReadFromJsonAsync(Stream jsonStream) + { + using (var reader = new StreamReader(jsonStream)) + { + var json = await reader.ReadToEndAsync(); + + var result = !string.IsNullOrWhiteSpace(json) + ? JsonConvert.DeserializeObject(json) + : null; + + if (result == null) + throw new Exception($"Failed to read {nameof(NotificationChannelConfig)} from json"); + + return result; + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/Dto/DistributedNotificationGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/Dto/DistributedNotificationGateway.cs new file mode 100644 index 0000000000..f691f37d8c --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/Dto/DistributedNotificationGateway.cs @@ -0,0 +1,25 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Notifications.Distribution.NotificationGateways.Dto +{ + /// + /// Distributed file template + /// + public class DistributedNotificationGateway : DistributedConfigurableItemBase + { + /// + /// + /// + public Guid PartOfId { get; set; } + + /// + /// + /// + public string GatewayTypeName { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayExport.cs new file mode 100644 index 0000000000..b20418dcca --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayExport.cs @@ -0,0 +1,12 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; + +namespace Shesha.Notifications.Distribution.NotificationGateways +{ + /// + /// file template import + /// + public interface INotificationGatewayExport : IConfigurableItemExport + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayImport.cs new file mode 100644 index 0000000000..d03e7de8a4 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/INotificationGatewayImport.cs @@ -0,0 +1,12 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; + +namespace Shesha.Notifications.Distribution.NotificationGateways +{ + /// + /// file template export + /// + public interface INotificationGatewayImport : IConfigurableItemImport + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayExport.cs new file mode 100644 index 0000000000..ce28198a6c --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayExport.cs @@ -0,0 +1,76 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Notifications.Distribution.NotificationGateways.Dto; +using Shesha.Notifications.Distribution.NotificationTypes.Dto; +using Shesha.Services; +using Shesha.StoredFiles; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Shesha.Notifications.Distribution.NotificationGateways +{ + /// + /// file template import + /// + public class NotificationGatewayExport : INotificationGatewayExport, ITransientDependency + { + private readonly IRepository _configurationRepo; + + public NotificationGatewayExport(IRepository configurationRepo) + { + _configurationRepo = configurationRepo; + } + + public string ItemType => NotificationGatewayConfig.ItemTypeName; + + public async Task ExportItemAsync(Guid id) + { + var item = await _configurationRepo.GetAsync(id); + return await ExportItemAsync(item); + } + + public async Task ExportItemAsync(ConfigurationItemBase item) + { + if (!(item is NotificationGatewayConfig itemConfig)) + throw new ArgumentException($"Wrong type of argument {item}. Expected {nameof(NotificationGatewayConfig)}, actual: {item.GetType().FullName}", nameof(item)); + + var result = new DistributedNotificationGateway + { + Id = itemConfig.Id, + Name = itemConfig.Name, + ModuleName = itemConfig.Module?.Name, + FrontEndApplication = itemConfig.Application?.AppKey, + ItemType = itemConfig.ItemType, + + Label = itemConfig.Label, + Description = itemConfig.Description, + OriginId = itemConfig.Origin?.Id, + BaseItem = itemConfig.BaseItem?.Id, + VersionNo = itemConfig.VersionNo, + VersionStatus = itemConfig.VersionStatus, + ParentVersionId = itemConfig.ParentVersion?.Id, + Suppress = itemConfig.Suppress, + + // specific properties + PartOfId = itemConfig.PartOf.Id, + GatewayTypeName = itemConfig.GatewayTypeName + }; + + return await Task.FromResult(result); + } + + /// inheritedDoc + public async Task WriteToJsonAsync(DistributedConfigurableItemBase item, Stream jsonStream) + { + var json = JsonConvert.SerializeObject(item, Formatting.Indented); + using (var writer = new StreamWriter(jsonStream)) + { + await writer.WriteAsync(json); + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayImport.cs new file mode 100644 index 0000000000..abeb80afc6 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationGateways/NotificationGatewayImport.cs @@ -0,0 +1,126 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Domain.ConfigurationItems; +using Shesha.EntityReferences; +using Shesha.Services; +using Shesha.Services.ConfigurationItems; +using System; +using System.IO; +using System.Threading.Tasks; +using Shesha.Notifications.Distribution.NotificationTypes.Dto; +using Shesha.Notifications.Distribution.NotificationGateways.Dto; + +namespace Shesha.Notifications.Distribution.NotificationGateways +{ + /// + /// file template import + /// + public class NotificationGatewayImport : ConfigurationItemImportBase, INotificationGatewayImport, ITransientDependency + { + public readonly IRepository _configurationRepo; + public readonly IRepository _channelConfigurationRepo; + + public NotificationGatewayImport( + IRepository moduleRepo, + IRepository frontEndAppRepo, + IRepository configurationRepo, + IRepository channelConfigurationRepo + ) : base (moduleRepo, frontEndAppRepo) + { + _configurationRepo = configurationRepo; + _channelConfigurationRepo = channelConfigurationRepo; + } + + public string ItemType => NotificationGatewayConfig.ItemTypeName; + + public async Task ImportItemAsync(DistributedConfigurableItemBase item, IConfigurationItemsImportContext context) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (!(item is DistributedNotificationGateway itemConfig)) + throw new NotSupportedException($"{this.GetType().FullName} supports only items of type {nameof(NotificationGatewayConfig)}. Actual type is {item.GetType().FullName}"); + + return await ImportAsync(itemConfig, context); + } + + protected async Task ImportAsync(DistributedNotificationGateway item, IConfigurationItemsImportContext context) + { + // use status specified in the context with fallback to imported value + var statusToImport = context.ImportStatusAs ?? item.VersionStatus; + + // get DB config + var dbItem = await _configurationRepo.FirstOrDefaultAsync(x => x.Name == item.Name && x.Description == item.Description + && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) + && x.IsLast); + + if (dbItem != null) + { + + // ToDo: Temporary update the current version. + // Need to update the rest of the other code to work with versioning first + + await MapConfigAsync(item, dbItem, context); + await _configurationRepo.UpdateAsync(dbItem); + } + else + { + dbItem = new NotificationGatewayConfig(); + await MapConfigAsync(item, dbItem, context); + + // fill audit? + dbItem.VersionNo = 1; + dbItem.Module = await GetModuleAsync(item.ModuleName, context); + + // important: set status according to the context + dbItem.VersionStatus = statusToImport; + dbItem.CreatedByImport = context.ImportResult; + + dbItem.Normalize(); + await _configurationRepo.InsertAsync(dbItem); + } + return dbItem; + } + + + protected async Task MapConfigAsync(DistributedNotificationGateway item, NotificationGatewayConfig dbItem, IConfigurationItemsImportContext context) + { + dbItem.Name = item.Name; + dbItem.Module = await GetModuleAsync(item.ModuleName, context); + dbItem.Application = await GetFrontEndAppAsync(item.FrontEndApplication, context); + dbItem.ItemType = item.ItemType; + + dbItem.Label = item.Label; + dbItem.Description = item.Description; + dbItem.VersionNo = item.VersionNo; + dbItem.VersionStatus = item.VersionStatus; + dbItem.Suppress = item.Suppress; + + // entity specific properties + dbItem.PartOf = await _channelConfigurationRepo.FirstOrDefaultAsync(x => x.Id == item.PartOfId); + dbItem.GatewayTypeName = item.GatewayTypeName; + + return dbItem; + } + + public async Task ReadFromJsonAsync(Stream jsonStream) + { + using (var reader = new StreamReader(jsonStream)) + { + var json = await reader.ReadToEndAsync(); + + var result = !string.IsNullOrWhiteSpace(json) + ? JsonConvert.DeserializeObject(json) + : null; + + if (result == null) + throw new Exception($"Failed to read {nameof(NotificationGatewayConfig)} from json"); + + return result; + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs new file mode 100644 index 0000000000..f089f677f0 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/Dto/DistributedNotificationType.cs @@ -0,0 +1,40 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Shesha.Notifications.Distribution.NotificationTypes.Dto +{ + /// + /// Distributed file template + /// + public class DistributedNotificationTypes: DistributedConfigurableItemBase + { + /// + /// + /// + public bool AllowAttachments { get; set; } + /// + /// + /// + public bool Disable { get; set; } + /// + /// If true indicates that users may opt out of this notification + /// + public bool CanOtpOut { get; set; } + /// + /// + /// + public string Category { get; set; } + /// + /// + /// + public int OrderIndex { get; set; } + /// + /// List of NotificationChannelConfigs + /// + [StringLength(int.MaxValue)] + public string OverrideChannels { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeExport.cs new file mode 100644 index 0000000000..34fade590d --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeExport.cs @@ -0,0 +1,12 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; + +namespace Shesha.Notifications.Distribution.NotificationTypes +{ + /// + /// file template import + /// + public interface INotificationTypeExport : IConfigurableItemExport + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeImport.cs new file mode 100644 index 0000000000..3dc6199921 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/INotificationTypeImport.cs @@ -0,0 +1,12 @@ +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; + +namespace Shesha.Notifications.Distribution.NotificationTypes +{ + /// + /// file template export + /// + public interface INotificationTypeImport : IConfigurableItemImport + { + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs new file mode 100644 index 0000000000..b70ebed5a6 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeExport.cs @@ -0,0 +1,79 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Notifications.Distribution.NotificationTypes.Dto; +using Shesha.Services; +using Shesha.StoredFiles; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Shesha.Notifications.Distribution.NotificationTypes +{ + /// + /// file template import + /// + public class NotificationTypeExport : INotificationTypeExport, ITransientDependency + { + private readonly IRepository _configurationRepo; + + public NotificationTypeExport(IRepository configurationRepo) + { + _configurationRepo = configurationRepo; + } + + public string ItemType => NotificationTypeConfig.ItemTypeName; + + public async Task ExportItemAsync(Guid id) + { + var item = await _configurationRepo.GetAsync(id); + return await ExportItemAsync(item); + } + + public async Task ExportItemAsync(ConfigurationItemBase item) + { + if (!(item is NotificationTypeConfig itemConfig)) + throw new ArgumentException($"Wrong type of argument {item}. Expected {nameof(NotificationTypeConfig)}, actual: {item.GetType().FullName}", nameof(item)); + + var result = new DistributedNotificationTypes + { + Id = itemConfig.Id, + Name = itemConfig.Name, + ModuleName = itemConfig.Module?.Name, + FrontEndApplication = itemConfig.Application?.AppKey, + ItemType = itemConfig.ItemType, + + Label = itemConfig.Label, + Description = itemConfig.Description, + OriginId = itemConfig.Origin?.Id, + BaseItem = itemConfig.BaseItem?.Id, + VersionNo = itemConfig.VersionNo, + VersionStatus = itemConfig.VersionStatus, + ParentVersionId = itemConfig.ParentVersion?.Id, + Suppress = itemConfig.Suppress, + + // specific properties + AllowAttachments = itemConfig.AllowAttachments, + Disable = itemConfig.Disable, + CanOtpOut = itemConfig.CanOtpOut, + Category = itemConfig.Category, + OrderIndex = itemConfig.OrderIndex, + OverrideChannels = itemConfig.OverrideChannels, + }; + + return await Task.FromResult(result); + } + + /// inheritedDoc + public async Task WriteToJsonAsync(DistributedConfigurableItemBase item, Stream jsonStream) + { + var json = JsonConvert.SerializeObject(item, Formatting.Indented); + using (var writer = new StreamWriter(jsonStream)) + { + await writer.WriteAsync(json); + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs new file mode 100644 index 0000000000..d7177d8055 --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Distribution/NotificationTypes/NotificationTypeImport.cs @@ -0,0 +1,126 @@ +using Abp.Dependency; +using Abp.Domain.Repositories; +using Newtonsoft.Json; +using Shesha.ConfigurationItems.Distribution; +using Shesha.Domain; +using Shesha.Domain.ConfigurationItems; +using Shesha.EntityReferences; +using Shesha.Services; +using Shesha.Services.ConfigurationItems; +using System; +using System.IO; +using System.Threading.Tasks; +using Shesha.Notifications.Distribution.NotificationTypes.Dto; + +namespace Shesha.Notifications.Distribution.NotificationTypes +{ + /// + /// file template import + /// + public class NotificationTypeImport : ConfigurationItemImportBase, INotificationTypeImport, ITransientDependency + { + public readonly IRepository _configurationRepo; + + public NotificationTypeImport( + IRepository moduleRepo, + IRepository frontEndAppRepo, + IRepository configurationRepo + ) : base (moduleRepo, frontEndAppRepo) + { + _configurationRepo = configurationRepo; + } + + public string ItemType => NotificationTypeConfig.ItemTypeName; + + public async Task ImportItemAsync(DistributedConfigurableItemBase item, IConfigurationItemsImportContext context) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (!(item is DistributedNotificationTypes itemConfig)) + throw new NotSupportedException($"{this.GetType().FullName} supports only items of type {nameof(NotificationTypeConfig)}. Actual type is {item.GetType().FullName}"); + + return await ImportAsync(itemConfig, context); + } + + protected async Task ImportAsync(DistributedNotificationTypes item, IConfigurationItemsImportContext context) + { + // use status specified in the context with fallback to imported value + var statusToImport = context.ImportStatusAs ?? item.VersionStatus; + + // get DB config + var dbItem = await _configurationRepo.FirstOrDefaultAsync(x => x.Name == item.Name && x.Description == item.Description + && (x.Module == null && item.ModuleName == null || x.Module != null && x.Module.Name == item.ModuleName) + && x.IsLast); + + if (dbItem != null) + { + + // ToDo: Temporary update the current version. + // Need to update the rest of the other code to work with versioning first + + await MapConfigAsync(item, dbItem, context); + await _configurationRepo.UpdateAsync(dbItem); + } + else + { + dbItem = new NotificationTypeConfig(); + await MapConfigAsync(item, dbItem, context); + + // fill audit? + dbItem.VersionNo = 1; + dbItem.Module = await GetModuleAsync(item.ModuleName, context); + + // important: set status according to the context + dbItem.VersionStatus = statusToImport; + dbItem.CreatedByImport = context.ImportResult; + + dbItem.Normalize(); + await _configurationRepo.InsertAsync(dbItem); + } + return dbItem; + } + + + protected async Task MapConfigAsync(DistributedNotificationTypes item, NotificationTypeConfig dbItem, IConfigurationItemsImportContext context) + { + dbItem.Name = item.Name; + dbItem.Module = await GetModuleAsync(item.ModuleName, context); + dbItem.Application = await GetFrontEndAppAsync(item.FrontEndApplication, context); + dbItem.ItemType = item.ItemType; + + dbItem.Label = item.Label; + dbItem.Description = item.Description; + dbItem.VersionNo = item.VersionNo; + dbItem.VersionStatus = item.VersionStatus; + dbItem.Suppress = item.Suppress; + + // entity specific properties + dbItem.AllowAttachments = item.AllowAttachments; + dbItem.Disable = item.Disable; + dbItem.CanOtpOut = item.CanOtpOut; + dbItem.Category = item.Category; + dbItem.OrderIndex = item.OrderIndex; + dbItem.OverrideChannels = item.OverrideChannels; + + return dbItem; + } + + public async Task ReadFromJsonAsync(Stream jsonStream) + { + using (var reader = new StreamReader(jsonStream)) + { + var json = await reader.ReadToEndAsync(); + + var result = !string.IsNullOrWhiteSpace(json) + ? JsonConvert.DeserializeObject(json) + : null; + + if (result == null) + throw new Exception($"Failed to read {nameof(NotificationTypeConfig)} from json"); + + return result; + } + } + } +} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/BroadcastNotificationJobArgs.cs similarity index 94% rename from shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs rename to shesha-core/src/Shesha.Application/Notifications/Dto/BroadcastNotificationJobArgs.cs index e711ad52b8..fcd91e7d7d 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/BroadcastNotificationJobArgs.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/BroadcastNotificationJobArgs.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Dto +namespace Shesha.Notifications.Dto { public class BroadcastNotificationJobArgs { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/DirectNotificationJobArgs.cs similarity index 85% rename from shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs rename to shesha-core/src/Shesha.Application/Notifications/Dto/DirectNotificationJobArgs.cs index 68f169e513..b01ff261f2 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/DirectNotificationJobArgs.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/DirectNotificationJobArgs.cs @@ -1,12 +1,11 @@ using Shesha.Domain; -using Shesha.Notifications.Dto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Dto +namespace Shesha.Notifications.Dto { public class DirectNotificationJobArgs { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs similarity index 90% rename from shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs rename to shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs index 78bd750561..e85464d581 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Dto/NotificationMessageDto.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Dto/NotificationMessageDto.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Dto +namespace Shesha.Notifications.Dto { public class NotificationMessageDto { diff --git a/shesha-core/src/Shesha.Application/Notifications/Dto/ShaNotificationData.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/ShaNotificationData.cs deleted file mode 100644 index 255a82a360..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Dto/ShaNotificationData.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using Abp.Notifications; -using Newtonsoft.Json; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.EntityReferences; - -namespace Shesha.Notifications.Dto -{ - /// - /// Is used to store template based notification data - /// - [Serializable] - public class ShaNotificationData : NotificationData - { - /// - /// Template Id - /// - public Guid? TemplateId { get; set; } - - /// - /// Attachments - /// - public List Attachments { get; set; } = new List(); - - /// - /// Send type (email/sms/push etc) - /// - public virtual RefListNotificationType SendType { get; set; } - - /// - /// Recipient text (email address/mobile number etc) - /// - public virtual string RecipientText { get; set; } - /// - /// Recipient text (email address/mobile number etc) - /// - public virtual string SourceEntityId { get; internal set; } - - public virtual string SourceEntityClassName { get; internal set; } - - public virtual string SourceEntityDisplayName { get; internal set; } - - public virtual string Cc { get; set; } - public virtual Guid? RecipientId { get; set; } - - protected string Data { - get => Properties[nameof(Data)] as string; - set => Properties[nameof(Data)] = value; - } - - protected string DataTypeName - { - get => Properties[nameof(DataTypeName)] as string; - set => Properties[nameof(DataTypeName)] = value; - } - - public void SetSourceEntity(GenericEntityReference sourceEntity = null) - { - if (sourceEntity != null) - { - SourceEntityId = sourceEntity.Id; - SourceEntityClassName = sourceEntity._className; - SourceEntityDisplayName = sourceEntity._displayName; - } - else - { - SourceEntityId = null; - SourceEntityClassName = null; - SourceEntityDisplayName = null; - } - } - - public NotificationData GetData() - { - if (string.IsNullOrWhiteSpace(Data) || string.IsNullOrWhiteSpace(DataTypeName)) - return null; - - var type = System.Type.GetType(DataTypeName); - - return type != null - ? JsonConvert.DeserializeObject(Data, type) as NotificationData - : null; - } - - public void SetData(NotificationData data) - { - Data = JsonConvert.SerializeObject(data); - DataTypeName = data.GetType().AssemblyQualifiedName; - } - - /// - /// Default constructor. Note: important for deserialization - /// - public ShaNotificationData() - { - - } - - public ShaNotificationData(NotificationData data) - { - SetData(data); - } - } -} diff --git a/shesha-core/src/Shesha.Application/Notifications/Dto/TestNotificationData.cs b/shesha-core/src/Shesha.Application/Notifications/Dto/TestNotificationData.cs deleted file mode 100644 index ff6724e7a8..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Dto/TestNotificationData.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Abp.Notifications; - -namespace Shesha.Notifications.Dto -{ - public class TestNotificationData: NotificationData - { - public string UserName { get; set; } - public string Greeting { get; set; } - public string CustomMessage { get; set; } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/EmailRealTimeNotifier.cs b/shesha-core/src/Shesha.Application/Notifications/EmailRealTimeNotifier.cs deleted file mode 100644 index 66c62e6ab4..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/EmailRealTimeNotifier.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Abp.Dependency; -using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Abp.Notifications; -using Hangfire; -using Shesha.Authorization.Users; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.Email; -using Shesha.Email.Dtos; -using Shesha.Extensions; -using Shesha.NHibernate; -using Shesha.NotificationMessages.Dto; -using Shesha.Services; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Shesha.Notifications -{ - /// - /// Email notifier - /// - public class EmailRealTimeNotifier : RealTimeNotifierBase, IShaRealTimeNotifier, ITransientDependency - { - private readonly ISheshaEmailSender _emailSender; - private readonly IRepository _attachmentRepository; - private readonly IStoredFileService _fileService; - - public EmailRealTimeNotifier(UserManager userManager, IRepository personRepository, IRepository templateRepository, IRepository notificationMessageRepository, IUnitOfWorkManager uowManager, IRepository notificationRepository, ISheshaEmailSender emailSender, IRepository attachmentRepository, IStoredFileService fileService) : base(userManager, personRepository, templateRepository, notificationMessageRepository, uowManager, notificationRepository) - { - _emailSender = emailSender; - _attachmentRepository = attachmentRepository; - _fileService = fileService; - } - - /// inheritedDoc - public override RefListNotificationType NotificationType => RefListNotificationType.Email; - - public bool UseOnlyIfRequestedAsTarget => true; - - /// inheritedDoc - public async Task SendNotificationsAsync(UserNotification[] userNotifications) - { - try - { - if (!userNotifications.Any()) - return; - - foreach (var userNotification in userNotifications) - { - // todo: use explicitly specified template with fallback to default - var template = await GetTemplateAsync(userNotification.Notification.NotificationName, RefListNotificationType.Email); - if (template == null || !template.IsEnabled) - continue; - - if (template.SendType != RefListNotificationType.Email) - throw new Exception($"Wrong type of template. Expected `{RefListNotificationType.Email}`, actual `{template.SendType}`"); - - var person = await GetRecipientAsync(userNotification); - var email = person?.GetEmail(); - if (string.IsNullOrWhiteSpace(email)) - continue; - - var subject = await GenerateContentAsync(template.Subject, userNotification); - var body = await GenerateContentAsync(template.Body, userNotification); - - if (string.IsNullOrWhiteSpace(subject) && string.IsNullOrWhiteSpace(body)) - continue; - - var messageId = await CreateNotificationMessageAsync(template.Id, person.Id, message => - { - message.Subject = subject; - message.Body = body; - message.RecipientText = email; - //message.SourceEntity = userNotification.; - }); - - // save attachments - - - // schedule sending - UowManager.Current.DoAfterTransaction(() => BackgroundJob.Enqueue(() => SendNotification(messageId))); - } - } - catch (Exception) - { - throw; - } - } - - private async Task> GetAttachmentsAsync(NotificationMessage message) - { - var attachments = await _attachmentRepository.GetAll().Where(a => a.Message == message).ToListAsync(); - - var result = attachments.Select(a => new EmailAttachment(a.FileName, _fileService.GetStream(a.File))).ToList(); - - return result; - } - - private List GetAttachments(NotificationMessage message) - { - var attachments = _attachmentRepository.GetAll().Where(a => a.Message == message).ToList(); - - var result = attachments.Select(a => new EmailAttachment(a.FileName, _fileService.GetStream(a.File))).ToList(); - - return result; - } - - /// inheritedDoc - protected override async Task DoSendNotificationMessageAsync(NotificationMessage message) - { - var attachments = await GetAttachmentsAsync(message); - - _emailSender.SendMail(null, message.RecipientText, message.Subject, message.Body, message.Template?.BodyFormat == RefListNotificationTemplateType.Html, attachments, message.Cc, throwException: true); - } - - protected override void DoSendNotificationMessage(NotificationMessage message) - { - var attachments = GetAttachments(message); - - _emailSender.SendMail(null, message.RecipientText, message.Subject, message.Body, message.Template?.BodyFormat == RefListNotificationTemplateType.Html, attachments, throwException: true); - } - - /// inheritedDoc - public async Task SendNotificationsAsync(List notificationMessages) - { - await SendNotificationsAsync(notificationMessages, RefListNotificationType.Email); - } - - public Task ResendMessageAsync(Guid notificationMessageId) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs similarity index 91% rename from shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs rename to shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs index 5a87464a7b..b7669ae1a1 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/EmailChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs @@ -7,10 +7,9 @@ using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Email.Dtos; -using Shesha.Notifications.Dto; -using Shesha.OmoNotifications.Configuration; -using Shesha.OmoNotifications.Configuration.Email; -using Shesha.OmoNotifications.Emails.Gateways; +using Shesha.Notifications.Configuration; +using Shesha.Notifications.Configuration.Email; +using Shesha.Notifications.Emails.Gateways; using Shesha.Utilities; using System; using System.Collections.Generic; @@ -20,7 +19,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications +namespace Shesha.Notifications { public class EmailChannelSender : INotificationChannelSender { @@ -68,7 +67,7 @@ private bool EmailsEnabled() return enabled; } - public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) { //if (message.Length > MaxMessageSize) // throw new ArgumentException("Message exceeds the maximum allowed size for Email."); diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs similarity index 94% rename from shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs rename to shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs index 49480d9975..079d5e0eea 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/EmailGatewayFactory.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Emails.Gateways +namespace Shesha.Notifications.Emails.Gateways { public class EmailGatewayFactory { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs similarity index 68% rename from shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs rename to shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs index 0b20a44eaf..ef36ef9bf9 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/IEmailGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs @@ -6,11 +6,11 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Emails.Gateways +namespace Shesha.Notifications.Emails.Gateways { public interface IEmailGateway { - Task SendAsync(string fromPerson, string toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null); + Task SendAsync(string fromPerson, string toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null); Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); } diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs similarity index 94% rename from shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs rename to shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs index 4308fb0179..eed40d099e 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Emails/Gateways/SmtpGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs @@ -1,7 +1,7 @@ using Shesha.Domain; using Shesha.Email.Dtos; -using Shesha.OmoNotifications.Configuration.Email; -using Shesha.OmoNotifications.Configuration.Email.Gateways; +using Shesha.Notifications.Configuration.Email; +using Shesha.Notifications.Configuration.Email.Gateways; using System; using System.Collections.Generic; using System.Linq; @@ -9,11 +9,11 @@ using System.Net; using System.Text; using System.Threading.Tasks; -using Shesha.OmoNotifications.Configuration; +using Shesha.Notifications.Configuration; using Castle.Core.Logging; using Shesha.Utilities; -namespace Shesha.OmoNotifications.Emails.Gateways +namespace Shesha.Notifications.Emails.Gateways { public class SmtpGateway : IEmailGateway { @@ -40,7 +40,7 @@ public SmtpGateway(IEmailGatewaySettings emailGatewaySettings, INotificationSett /// /// /// - public Task SendAsync(string fromPerson, string toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + public Task SendAsync(string fromPerson, string toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) { // Send email via SMTP or email service provider using (var mail = BuildMessageWith(fromPerson, toPerson, message.Subject, message.Message, isBodyHtml, cc)) diff --git a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationFailedWaitRetryException.cs b/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationFailedWaitRetryException.cs deleted file mode 100644 index 9b551fd303..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationFailedWaitRetryException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Abp.Notifications; -using System; - -namespace Shesha.Notifications.Exceptions -{ - /// - /// Notification failed and awaiting for retry exception - /// - public class ShaNotificationFailedWaitRetryException : Exception - { - public Guid NotificationId { get; private set; } - public ShaNotificationFailedWaitRetryException(Guid notificationId): base($"Failed to send notification {notificationId}, waiting for retry") - { - NotificationId = notificationId; - } - } -} diff --git a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationIsStillPreparingException.cs b/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationIsStillPreparingException.cs deleted file mode 100644 index 16b79f14a9..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationIsStillPreparingException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Abp.Notifications; -using System; - -namespace Shesha.Notifications.Exceptions -{ - /// - /// Notification is still preparing exception - /// - public class ShaNotificationIsStillPreparingException : Exception - { - public Guid NotificationId { get; private set; } - public ShaNotificationIsStillPreparingException(Guid notificationId): base($"Notification with id = '{notificationId}' is still preparing") - { - NotificationId = notificationId; - } - } -} diff --git a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationNotFoundException.cs b/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationNotFoundException.cs deleted file mode 100644 index 7801d1b935..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationNotFoundException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Shesha.Notifications.Exceptions -{ - /// - /// Notification not found exception - /// - public class ShaNotificationNotFoundException : Exception - { - public Guid NotificationId { get; private set; } - public ShaNotificationNotFoundException(Guid notificationId): base($"Notification with id = '{notificationId}' not found") - { - NotificationId = notificationId; - } - } -} diff --git a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationSaveFailedException.cs b/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationSaveFailedException.cs deleted file mode 100644 index 0a28a1cabb..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Exceptions/ShaNotificationSaveFailedException.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Abp.Notifications; -using System; - -namespace Shesha.Notifications.Exceptions -{ - /// - /// Notification save failed exception - /// - public class ShaNotificationSaveFailedException: Exception - { - public string NotificationName { get; set; } - public NotificationData NotificationData { get; set; } - - public ShaNotificationSaveFailedException(string notificationName, NotificationData notificationData): base("Failed to save notification") - { - NotificationName = notificationName; - NotificationData = notificationData; - } - } -} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs b/shesha-core/src/Shesha.Application/Notifications/Helpers/MobileHelper.cs similarity index 96% rename from shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs rename to shesha-core/src/Shesha.Application/Notifications/Helpers/MobileHelper.cs index 8ad2d61ec3..56d1601791 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/MobileHelper.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Helpers/MobileHelper.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Helpers +namespace Shesha.Notifications.Helpers { public static class MobileHelper { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs b/shesha-core/src/Shesha.Application/Notifications/Helpers/TemplateHelper.cs similarity index 96% rename from shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs rename to shesha-core/src/Shesha.Application/Notifications/Helpers/TemplateHelper.cs index e61f1bac33..4d2d297b34 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Helpers/TemplateHelper.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Helpers/TemplateHelper.cs @@ -3,7 +3,7 @@ using System.Reflection; using System.Text.RegularExpressions; -namespace Shesha.OmoNotifications.Helpers +namespace Shesha.Notifications.Helpers { public static class TemplateHelper { diff --git a/shesha-core/src/Shesha.Application/Notifications/INotificationAppService.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationAppService.cs deleted file mode 100644 index 0650082e18..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/INotificationAppService.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Abp.Notifications; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.DynamicEntities.Dtos; -using Shesha.EntityReferences; -using Shesha.Notifications.Dto; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Shesha.Notifications -{ - /// - /// Notification service - /// - public interface INotificationAppService: IDynamicCrudAppService, Guid> - { - /// - /// Publish new notification - /// - /// Name of the notification - /// Notification data (is used in templates) - /// list of recipients - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishAsync(string notificationName, NotificationData data, List recipients, GenericEntityReference sourceEntity = null); - - /// - /// Publish email notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// Recipient email address - /// Notification attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishEmailNotificationAsync(string notificationName, TData data, string emailAddress, List attachments, GenericEntityReference sourceEntity = null) where TData : NotificationData; - - /// - /// Publish email notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// The person the email should go to - /// Notification attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishEmailNotificationAsync(string notificationName, TData data, Person recipient, List attachments, GenericEntityReference sourceEntity = null) where TData : NotificationData; - - /// - /// Publish email notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Recipient email address - /// Notification attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - /// - Task PublishEmailNotificationAsync(Guid templateId, TData data, string emailAddress, List attachments, GenericEntityReference sourceEntity = null, string cc = "") where TData : NotificationData; - - /// - /// Publish email notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Receiptient of the notification - /// Notification attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - /// - Task PublishEmailNotificationAsync(Guid templateId, TData data, Person recipient, List attachments, GenericEntityReference sourceEntity = null, string cc = "") where TData : NotificationData; - - /// - /// Publish sms notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// Recipient mobile number - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishSmsNotificationAsync(string notificationName, TData data, string mobileNo, GenericEntityReference sourceEntity = null) where TData : NotificationData; - - - /// - /// Publish sms notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// Recipient of the notification - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishSmsNotificationAsync(string notificationName, TData data, Person recipient, GenericEntityReference sourceEntity = null) where TData : NotificationData; - - - /// - /// Publish sms notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Recipient mobile number - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishSmsNotificationAsync(Guid templateId, TData data, string mobileNo, GenericEntityReference sourceEntity = null) where TData : NotificationData; - - /// - /// Publish sms notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Recipient of the notification - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - Task PublishSmsNotificationAsync(Guid templateId, TData data, Person recipient, GenericEntityReference sourceEntity = null) where TData : NotificationData; - - /// - /// Save notification attachment - /// - Task SaveAttachmentAsync(string fileName, Stream stream); - - /// - /// Send Notification to specified person - /// - Task SendNotification(string notificationName, Person recipient, TData data, RefListNotificationType? notificationType = null, GenericEntityReference sourceEntity = null, List attachments = null, string cc = "") where TData : NotificationData; - } -} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs similarity index 67% rename from shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs rename to shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs index 2b49ac864f..386e519c21 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/INotificationChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs @@ -1,8 +1,7 @@ using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Email.Dtos; -using Shesha.Notifications.Dto; -using Shesha.OmoNotifications.Configuration.Email; +using Shesha.Notifications.Configuration.Email; using System; using System.Collections.Generic; using System.Linq; @@ -10,12 +9,12 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications +namespace Shesha.Notifications { public interface INotificationChannelSender { string GetRecipientId(Person person); - Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc, bool throwException = false, List attachments = null); + Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml, string cc, bool throwException = false, List attachments = null); Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null); } } diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs similarity index 53% rename from shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs rename to shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs index 7818f1383c..4299a94fb3 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/INotificationSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationSender.cs @@ -1,15 +1,14 @@ using Shesha.Domain; -using Shesha.Notifications.Dto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications +namespace Shesha.Notifications { public interface INotificationSender { - Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml); + Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml); } } diff --git a/shesha-core/src/Shesha.Application/Notifications/IShaNotificationDistributer.cs b/shesha-core/src/Shesha.Application/Notifications/IShaNotificationDistributer.cs deleted file mode 100644 index f0017acab9..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/IShaNotificationDistributer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; -using Abp.Notifications; -using Shesha.Domain; -using Shesha.NotificationMessages.Dto; - -namespace Shesha.Notifications -{ - /// - /// Shesha notifications distributer - /// - public interface IShaNotificationDistributer: INotificationDistributer - { - /// - /// Re-send with the specified Id - /// - Task ResendMessageAsync(NotificationMessageDto notificationMessage); - } -} diff --git a/shesha-core/src/Shesha.Application/Notifications/IShaRealTimeNotifier.cs b/shesha-core/src/Shesha.Application/Notifications/IShaRealTimeNotifier.cs deleted file mode 100644 index 9594d62aa1..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/IShaRealTimeNotifier.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Abp.Notifications; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.NotificationMessages.Dto; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Shesha.Notifications -{ - /// - /// Shesha real time notifier - /// - public interface IShaRealTimeNotifier: IRealTimeNotifier - { - /// - /// Send template-based notifications - /// - Task SendNotificationsAsync(List notificationMessages); - - /// - /// Re-send with the specified Id - /// - Task ResendMessageAsync(NotificationMessageDto notificationMessage); - - /// - /// Type of notifications processed by this notifier - /// - RefListNotificationType NotificationType { get; } - } -} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs b/shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs similarity index 82% rename from shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs rename to shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs index 5ad0870792..8421889858 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/BroadcastNotificationJobQueuer.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs @@ -9,7 +9,6 @@ using Shesha.Email.Dtos; using Shesha.NHibernate; using Shesha.Notifications.Dto; -using Shesha.OmoNotifications.Dto; using Shesha.Services; using System; using System.Collections.Generic; @@ -18,17 +17,17 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Jobs +namespace Shesha.Notifications.Jobs { public class BroadcastNotificationJobQueuer : AsyncBackgroundJob, ITransientDependency { private readonly IRepository _notificationTopicRepository; - private readonly IRepository _notificationRepository; - private readonly IRepository _notificationTemplateRepository; + private readonly IRepository _notificationRepository; + private readonly IRepository _notificationTemplateRepository; private readonly IRepository _notificationChannelRepository; private readonly IRepository _userTopicSubscriptionRepository; - private readonly IRepository _notificationMessage; - private readonly IRepository _attachmentRepository; + private readonly IRepository _notificationMessage; + private readonly IRepository _attachmentRepository; private readonly IEnumerable _channelSenders; private readonly IRepository _storedFileRepository; private readonly IStoredFileService _fileService; @@ -37,13 +36,13 @@ public class BroadcastNotificationJobQueuer : AsyncBackgroundJob notificationTopicRepository, - IRepository notificationRepository, + IRepository notificationRepository, IRepository notificationChannelRepository, IRepository storedFileRepository, - IRepository attachmentRepository, - IRepository notificationTemplateRepository, + IRepository attachmentRepository, + IRepository notificationTemplateRepository, IRepository userTopicSubscriptionRepository, - IRepository notificationMessage, + IRepository notificationMessage, IEnumerable channelSenders, IStoredFileService fileService, ISessionProvider sessionProvider, @@ -98,7 +97,7 @@ public override async Task ExecuteAsync(BroadcastNotificationJobArgs args) /// /// /// - public async Task SaveUserNotificationMessagesAsync(OmoNotification notification, MessageTemplate template, NotificationChannelConfig channel, string subject, string message, List attachments) + public async Task SaveUserNotificationMessagesAsync(Notification notification, NotificationTemplate template, NotificationChannelConfig channel, string subject, string message, List attachments) { var users = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == notification.NotificationTopic.Id); var notificationMessageDto = new NotificationMessageDto() @@ -109,7 +108,7 @@ public async Task SaveUserNotificationMessagesAsync(OmoN foreach (var user in users) { - var notificationMessage = new OmoNotificationMessage + var notificationMessage = new NotificationMessage { Subject = subject, Message = message, @@ -127,9 +126,9 @@ public async Task SaveUserNotificationMessagesAsync(OmoN foreach (var attachmentDto in attachments) { var file = await _storedFileRepository.GetAsync(attachmentDto.StoredFileId); - var attachment = new OmoNotificationMessageAttachment + var attachment = new NotificationMessageAttachment { - PartOf = notificationMessage, + Message = notificationMessage, File = file, FileName = attachmentDto.FileName }; diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs b/shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs similarity index 83% rename from shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs rename to shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs index 0ccf60bac6..c5ae75a1e8 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Jobs/DirectNotificationJobQueuer.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs @@ -4,23 +4,23 @@ using Abp.Domain.Uow; using Shesha.Domain; using Shesha.DynamicEntities.Dtos; -using Shesha.OmoNotifications.Dto; +using Shesha.Notifications.Dto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Jobs +namespace Shesha.Notifications.Jobs { public class DirectNotificationJobQueuer : AsyncBackgroundJob, ITransientDependency { private readonly IRepository _personRepository; - private readonly IRepository _notificationMessage; + private readonly IRepository _notificationMessage; private readonly IEnumerable _channelSenders; - public DirectNotificationJobQueuer(IRepository personRepository, IRepository notificationMessage, IEnumerable channelSenders) + public DirectNotificationJobQueuer(IRepository personRepository, IRepository notificationMessage, IEnumerable channelSenders) { _notificationMessage = notificationMessage; _personRepository = personRepository; diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs index ac7e2290a2..180c87bbfe 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs @@ -1,536 +1,341 @@ -using Abp; +using Abp.Application.Services.Dto; using Abp.BackgroundJobs; using Abp.Domain.Entities; using Abp.Domain.Repositories; using Abp.Notifications; +using Abp.UI; +using Hangfire; +using Hangfire.Storage; +using Newtonsoft.Json.Linq; +using NHibernate.Linq; using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.DynamicEntities.Dtos; using Shesha.EntityReferences; using Shesha.Notifications.Dto; +using Shesha.Notifications.Configuration; +using Shesha.Notifications.Helpers; +using Shesha.Notifications.Jobs; using Shesha.Services; using System; using System.Collections.Generic; -using System.IO; using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; -namespace Shesha.Notifications; - -/// -/// Notification application service -/// -public class NotificationAppService : DynamicCrudAppService, Guid>, INotificationAppService +namespace Shesha.Notifications { - private readonly INotificationPublisher _notificationPublisher; - private readonly IBackgroundJobManager _backgroundJobManager; - private readonly IStoredFileService _fileService; - private readonly INotificationPublicationContext _notificationPublicationContext; - private readonly IRepository _repository; - private readonly IRepository _templateRepository; - private readonly IRepository _personRepository; - - public NotificationAppService(IRepository repository, INotificationPublisher notificationPublisher, IBackgroundJobManager backgroundJobManager, IStoredFileService fileService, INotificationPublicationContext notificationPublicationContext, IRepository templateRepository, IRepository personRepository) : base(repository) - { - _notificationPublisher = notificationPublisher; - _backgroundJobManager = backgroundJobManager; - _fileService = fileService; - _notificationPublicationContext = notificationPublicationContext; - _repository = repository; - _templateRepository = templateRepository; - _personRepository = personRepository; - - } - - /// inheritedDoc - public async Task PublishAsync(string notificationName, - NotificationData data, - List recipients, - GenericEntityReference sourceEntity = null) - { - if (recipients == null) - throw new Exception($"{nameof(recipients)} must not be null"); - - var entityIdentifier = GetEntityIdentifier(sourceEntity); - - var userIds = recipients.Where(p => p.User != null) - .Select(p => new UserIdentifier(AbpSession.TenantId, p.User.Id)) - .ToArray(); - if (!userIds.Any()) - return; - - await _notificationPublisher.PublishAsync(notificationName, - data, - userIds: userIds, - entityIdentifier: entityIdentifier - ); - } - - - #region Direct email notifications - - /// - /// Publish email notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// Recipient email address - /// Notification attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - public async Task PublishEmailNotificationAsync(string notificationName, - TData data, - string emailAddress, - List attachments = null, - GenericEntityReference sourceEntity = null) where TData : NotificationData - { - if (string.IsNullOrWhiteSpace(emailAddress)) - throw new Exception($"{nameof(emailAddress)} must not be null"); - - var entityIdentifier = GetEntityIdentifier(sourceEntity); - - var wrappedData = new ShaNotificationData(data) - { - SendType = RefListNotificationType.Email, - RecipientText = emailAddress, - Attachments = attachments, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync(notificationName, wrappedData, entityIdentifier); - } - - - /// - /// Publish email notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// The person the email should go to - /// Notification attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - public async Task PublishEmailNotificationAsync(string notificationName, - TData data, - Person recipient, - List attachments = null, - GenericEntityReference sourceEntity = null) where TData : NotificationData + public class NotificationAppService: SheshaAppServiceBase { - - if (recipient == null) - throw new Exception($"{nameof(recipient)} must not be null"); - - if (string.IsNullOrWhiteSpace(recipient.EmailAddress1) && string.IsNullOrWhiteSpace(recipient.EmailAddress2)) - throw new Exception($"No email address available for {recipient.FullName}"); - - var emailAddress = string.IsNullOrWhiteSpace(recipient.EmailAddress1) ? recipient.EmailAddress2 : recipient.EmailAddress1; - var entityIdentifier = GetEntityIdentifier(sourceEntity); - - var wrappedData = new ShaNotificationData(data) + private readonly IEnumerable _channelSenders; + private readonly IRepository _notificationTypeRepository; + private readonly IRepository _notificationChannelRepository; + private readonly IRepository _messageTemplateRepository; + private readonly IRepository _notificationMessageRepository; + private readonly IRepository _userNotificationPreference; + private readonly IRepository _userTopicSubscriptionRepository; + private readonly IRepository _notificationTopicRepository; + private readonly IRepository _storedFileRepository; + private readonly IRepository _personRepository; + private readonly INotificationSettings _notificationSettings; + private readonly IStoredFileService _storedFileService; + private readonly IBackgroundJobManager _backgroundJobManager; + + public NotificationAppService(IEnumerable channelSenders, + IRepository notificationTypeRepository, + IRepository notificationChannelRepository, + IRepository userNotificationPreference, + IRepository messageTemplateRepository, + IRepository personRepository, + IRepository storedFileRepository, + IRepository userTopicSubscriptionRepository, + IStoredFileService storedFileService, + IRepository notificationMessageRepository, + IRepository notificationTopicRepository, + INotificationSettings notificationSettings, + IBackgroundJobManager backgroundJobManager) + { - SendType = RefListNotificationType.Email, - RecipientText = emailAddress, - Attachments = attachments, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync(notificationName, wrappedData, entityIdentifier); - } - - /// - /// Publish email notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Recipient email address - /// Attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - /// - public async Task PublishEmailNotificationAsync(Guid templateId, - TData data, - string emailAddress, - List attachments = null, - GenericEntityReference sourceEntity = null, - string cc = "") where TData : NotificationData - { - if (string.IsNullOrWhiteSpace(emailAddress)) - throw new Exception($"{nameof(emailAddress)} must not be null"); - - var entityIdentifier = GetEntityIdentifier(sourceEntity); - - var wrappedData = new ShaNotificationData(data) - { - SendType = RefListNotificationType.Email, - RecipientText = emailAddress, - TemplateId = templateId, - Attachments = attachments, - Cc = cc, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync("DirectEmail", wrappedData, entityIdentifier); - } - - - /// - /// Publish email notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Receiptient of the notification - /// Attachments - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - /// - public async Task PublishEmailNotificationAsync(Guid templateId, - TData data, - Person recipient, - List attachments = null, - GenericEntityReference sourceEntity = null, - string cc = "") where TData : NotificationData - { - if (recipient == null) - throw new Exception($"{nameof(recipient)} must not be null"); - - if (string.IsNullOrWhiteSpace(recipient.EmailAddress1) && string.IsNullOrWhiteSpace(recipient.EmailAddress2)) - throw new Exception($"No email address available for {recipient.FullName}"); - - var emailAddress = string.IsNullOrWhiteSpace(recipient.EmailAddress1) ? recipient.EmailAddress2 : recipient.EmailAddress1; - var entityIdentifier = GetEntityIdentifier(sourceEntity); - var wrappedData = new ShaNotificationData(data) - { - SendType = RefListNotificationType.Email, - RecipientId = recipient?.Id ?? null, - RecipientText = emailAddress, - TemplateId = templateId, - Attachments = attachments, - Cc = cc, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync("DirectEmail", wrappedData, entityIdentifier); - } - - #endregion - - #region Direct sms notifications - - /// - /// Publish sms notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// Recipient mobile number - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - public async Task PublishSmsNotificationAsync(string notificationName, - TData data, - string mobileNo, - GenericEntityReference sourceEntity = null) where TData : NotificationData - { - if (string.IsNullOrWhiteSpace(mobileNo)) - throw new Exception($"{nameof(mobileNo)} must not be null"); - - var entityIdentifier = GetEntityIdentifier(sourceEntity); - - var wrappedData = new ShaNotificationData(data) - { - SendType = RefListNotificationType.SMS, - RecipientText = mobileNo, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync(notificationName, wrappedData, entityIdentifier); - } - - /// - /// Publish sms notification - /// - /// Name of the notification. Default email template of the specified notification will be used - /// Data that is used to fill template - /// Receiptient of the notification - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - public async Task PublishSmsNotificationAsync(string notificationName, - TData data, - Person recipient, - GenericEntityReference sourceEntity = null) where TData : NotificationData - { - if (recipient == null) - throw new Exception($"{nameof(recipient)} must not be null"); - - if (string.IsNullOrWhiteSpace(recipient.MobileNumber1) && string.IsNullOrWhiteSpace(recipient.MobileNumber2)) - throw new Exception($"No mobile number available for {recipient.FullName}"); - - var mobileNumber = string.IsNullOrWhiteSpace(recipient.MobileNumber1) ? recipient.MobileNumber2 : recipient.MobileNumber1; - var entityIdentifier = GetEntityIdentifier(sourceEntity); - var wrappedData = new ShaNotificationData(data) - { - SendType = RefListNotificationType.SMS, - RecipientId = recipient?.Id ?? null, - RecipientText = mobileNumber, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync(notificationName, wrappedData, entityIdentifier); - } - - /// - /// Publish sms notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Recipient mobile number - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - public async Task PublishSmsNotificationAsync(Guid templateId, - TData data, - string mobileNo, - GenericEntityReference sourceEntity = null - ) where TData : NotificationData - { - if (string.IsNullOrWhiteSpace(mobileNo)) - throw new Exception($"{nameof(mobileNo)} must not be null"); - - var entityIdentifier = GetEntityIdentifier(sourceEntity); + _channelSenders = channelSenders; + _notificationTypeRepository = notificationTypeRepository; + _notificationChannelRepository = notificationChannelRepository; + _userNotificationPreference = userNotificationPreference; + _messageTemplateRepository = messageTemplateRepository; + _notificationSettings = notificationSettings; + _personRepository = personRepository; + _storedFileService = storedFileService; + _notificationMessageRepository = notificationMessageRepository; + _userTopicSubscriptionRepository = userTopicSubscriptionRepository; + _backgroundJobManager = backgroundJobManager; + _storedFileRepository = storedFileRepository; + _notificationTopicRepository = notificationTopicRepository; + } - var wrappedData = new ShaNotificationData(data) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task>> SendBroadcastNotification(NotificationTypeConfig type, NotificationTopic topic, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData: NotificationData { - SendType = RefListNotificationType.SMS, - RecipientText = mobileNo, - TemplateId = templateId, - }; + var notification = await SaveOrUpdateEntityAsync(null, item => + { + item.NotificationType = type; + item.NotificationTopic = topic; + item.NotificationData = data.ToString(); + item.Priority = (RefListNotificationPriority)priority; + item.TriggeringEntity = triggeringEntity; + }); - wrappedData.SetSourceEntity(sourceEntity); + await CurrentUnitOfWork.SaveChangesAsync(); - await _notificationPublisher.PublishAsync(templateId.ToString(), wrappedData, entityIdentifier); - } + var users = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == topic.Id); - /// - /// Publish sms notification using explicitly specified template - /// - /// Id of the template - /// Data that is used to fill template - /// Receiptient of the notification - /// Optional parameter. If notification is an Entity level notification, specifies the entity the notification relates to. - /// - public async Task PublishSmsNotificationAsync(Guid templateId, - TData data, - Person recipient, - GenericEntityReference sourceEntity = null - ) where TData : NotificationData - { - if (recipient == null) - throw new Exception($"{nameof(recipient)} must not be null"); + if (channel != null) + { + var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channel.SupportedFormat == x.MessageFormat); + var subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data); + var message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); + + await _backgroundJobManager.EnqueueAsync(new BroadcastNotificationJobArgs() + { + TemplateId = template.Id, + NotificationId = notification.Id, + ChannelId = channel.Id, + Subject = subject, + Message = message, + Attachments = attachments + }); + } + else + { + var subscriptions = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == topic.Id); + + if (subscriptions != null && subscriptions.Any()) + { + foreach (var user in users) + { + var userChannels = await GetChannelsAsync(type, user.User, priority); + + foreach (var channelConfig in userChannels) + { + var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); + var subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data); + var message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); + + await _backgroundJobManager.EnqueueAsync(new BroadcastNotificationJobArgs() + { + TemplateId = template.Id, + NotificationId = notification.Id, + ChannelId = channelConfig.Id, + Subject = subject, + Message = message, + Attachments = attachments + }); + } + } + + } + } - if (string.IsNullOrWhiteSpace(recipient.MobileNumber1) && string.IsNullOrWhiteSpace(recipient.MobileNumber2)) - throw new Exception($"No mobile number available for {recipient.FullName}"); + return await MapToDynamicDtoListAsync(new List()); + } - var mobileNumber = string.IsNullOrWhiteSpace(recipient.MobileNumber1) ? recipient.MobileNumber2 : recipient.MobileNumber1; - var entityIdentifier = GetEntityIdentifier(sourceEntity); - var wrappedData = new ShaNotificationData(data) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task>> SendNotification(NotificationTypeConfig type, Person fromPerson, Person toPerson, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData: NotificationData { - SendType = RefListNotificationType.SMS, - RecipientId = recipient?.Id ?? null, - RecipientText = mobileNumber, - TemplateId = templateId, - }; - - wrappedData.SetSourceEntity(sourceEntity); - - await _notificationPublisher.PublishAsync(templateId.ToString(), wrappedData, entityIdentifier); - } - - //private static EntityIdentifier GetEntityIdentifier(object entity) - //{ - // EntityIdentifier entityIdentifier = null; - // if (entity is not null) - // { - // if (entity is Entity) - // entityIdentifier = new EntityIdentifier(entity.GetType(), ((Entity)entity).Id); - // else if (entity is Entity) - // entityIdentifier = new EntityIdentifier(entity.GetType(), ((Entity)entity).Id); - // else if (entity is Entity) - // entityIdentifier = new EntityIdentifier(entity.GetType(), ((Entity)entity).Id); - // } - - // return entityIdentifier; - //} - - private static EntityIdentifier GetEntityIdentifier(GenericEntityReference genericEntity) - { - EntityIdentifier entityIdentifier = null; + var notification = await SaveOrUpdateEntityAsync(null, item => + { + item.NotificationType = type; + item.FromPerson = fromPerson; + item.ToPerson = toPerson; + item.NotificationData = data.ToString(); + item.TriggeringEntity = triggeringEntity; + item.Priority = (RefListNotificationPriority)priority; + }); - if (genericEntity != null) - { - Entity entity = genericEntity; - entityIdentifier = new EntityIdentifier(entity.GetType(), entity.Id); - } + await CurrentUnitOfWork.SaveChangesAsync(); - return entityIdentifier; - } + if (channel != null) + { + // Send notification to a specific channel + await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority,channel, attachments); + } + else + { + // Send notification to all determined channels + var channels = await GetChannelsAsync(type, toPerson, (RefListNotificationPriority)priority); - #endregion + foreach (var channelConfig in channels) + { + await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority, channelConfig, attachments); + } + } - /// - /// Note: for temporary usage only - /// - /// - /// - public async Task ReSendAbpNotification(List ids) - { - foreach (var id in ids) - { - await _backgroundJobManager.EnqueueAsync( - new NotificationDistributionJobArgs( - id - ) - ); + // Return the list of channels used for sending notifications as DynamicDto + return await MapToDynamicDtoListAsync(new List()); } - } - /// - /// Save notification attachment - /// - public async Task SaveAttachmentAsync(string fileName, Stream stream) - { - var file = await _fileService.SaveFileAsync(stream, fileName); - return new NotificationAttachmentDto() + /// + /// + /// + /// + /// + /// + /// + private async Task> GetChannelsAsync(NotificationTypeConfig type, Person recipient, RefListNotificationPriority priority) { - FileName = fileName, - StoredFileId = file.Id - }; - } + List results = new List(); - #region new notififiers - /// - /// Send Notification to specified person - /// - public async Task SendNotification(string notificationName, Person recipient, TData data, RefListNotificationType? notificationType = null, GenericEntityReference sourceEntity = null, List attachments = null, string cc = "") where TData : NotificationData - { + // Step 1: Check User Notification Preferences + var userPreferences = await _userNotificationPreference.GetAllListAsync( + x => x.User.Id == recipient.Id && x.NotificationType.Id == type.Id + ); - if (notificationType != null) - { - return await SendNotificationByType(notificationName, (int)notificationType, recipient, data, sourceEntity, attachments, cc); - } - else - { - if (recipient.PreferredContactMethod != null) + if (userPreferences != null && userPreferences.Any()) { - return await SendNotificationByType(notificationName, (int)recipient.PreferredContactMethod, recipient, data, sourceEntity, attachments, cc); + // Flatten and return DefaultChannel from user preferences if available + return userPreferences.Select(x => x.DefaultChannel).ToList(); } - else + + // Step 2: Check Override Channels in NotificationTypeConfig + if (!string.IsNullOrEmpty(type.OverrideChannels)) { - await SendNotificationByType(notificationName, (int)RefListNotificationType.SMS, recipient, data, sourceEntity, attachments); - return await SendNotificationByType(notificationName, (int)RefListNotificationType.Email, recipient, data, sourceEntity, attachments, cc); + try + { + var overrideChannelIds = JsonSerializer.Deserialize(type.OverrideChannels); + + // Fetch channels by IDs if the repository supports it + var channels = await _notificationChannelRepository + .GetAll() + .Where(channel => overrideChannelIds.Contains(channel.Id)) + .ToListAsync(); + + if (channels.Any()) + { + return channels; + } + } + catch (JsonException ex) + { + new UserFriendlyException("Error deserializing override channels", ex); + // Log deserialization error (ex.Message) and continue to fallback + // Optionally handle the error depending on requirements + } + return results; } - } - } - private async Task SendNotificationByType(string notificationName, int notificationType, Person person, TData data, GenericEntityReference sourceEntity = null, List attachments = null, string cc = "") where TData : NotificationData - { - var notification = await _repository.FirstOrDefaultAsync(e => e.Name == notificationName); - var templates = await _templateRepository.GetAllListAsync(e => e.Notification == notification); - - var template = templates.FirstOrDefault(e => (int)e.SendType == notificationType); - if (template != null) - { - switch (notificationType) + // Step 3: Fallback - Return default channels based on priority (if applicable) + var notificationSettings = await _notificationSettings.NotificationSettings.GetValueAsync(); + switch (priority) { - case (int)RefListNotificationType.Email: - return await SendEmailAsync(template.Id, recipient: person, data, sourceEntity: sourceEntity, attachments: attachments, cc: cc); - case (int)RefListNotificationType.SMS: - return await SendSmsAsync(template.Id, recipient: person, data, sourceEntity: sourceEntity); + case RefListNotificationPriority.Low: + return notificationSettings.Low; + case RefListNotificationPriority.Medium: + return notificationSettings.Medium; + case RefListNotificationPriority.High: + return notificationSettings.High; default: - break; - } + return new List(); + }; } - return null; - } - - private async Task SendSmsAsync(Guid notificationTemplate, string mobileNumber, TData data, GenericEntityReference sourceEntity = null, Person recipient = null) where TData : NotificationData - { - Guid? messageId = Guid.Empty; - - using (_notificationPublicationContext.BeginScope()) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private async Task ProcessAndSendNotificationToChannel(Notification notification, TData data, Person fromPerson, Person toPerson, NotificationTypeConfig type, RefListNotificationPriority priority, NotificationChannelConfig channelConfig, List attachments = null) where TData: NotificationData { - await PublishSmsNotificationAsync( - templateId: notificationTemplate, - data: data, - mobileNo: mobileNumber, - sourceEntity: sourceEntity - ); + var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); - messageId = _notificationPublicationContext.Statistics.NotificationMessages.FirstOrDefault()?.Id; - } - - return messageId; - } - - private async Task SendSmsAsync(Guid notificationTemplate, Person recipient, TData data, GenericEntityReference sourceEntity = null) where TData : NotificationData - { - Guid? messageId = Guid.Empty; + if (template == null) + throw new UserFriendlyException($"There is no {type.Name} template found for the {channelConfig.Name} channel"); - using (_notificationPublicationContext.BeginScope()) - { - await PublishSmsNotificationAsync( - templateId: notificationTemplate, - data: data, - sourceEntity: sourceEntity, - recipient: recipient - ); + var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == channelConfig.SenderTypeName); - messageId = _notificationPublicationContext.Statistics.NotificationMessages.FirstOrDefault()?.Id; - } + if (senderChannelInterface == null) + throw new UserFriendlyException($"Sender not found for channel {channelConfig.Name}"); - return messageId; - } + // Create a new notification message + var message = await SaveOrUpdateEntityAsync(null, item => + { + item.PartOf = notification; + item.Channel = channelConfig; + item.Subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate,data); + item.Message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); + item.RetryCount = 0; + item.ReadStatus = RefListNotificationReadStatus.Unread; + item.Direction = RefListNotificationDirection.Outgoing; + item.Status = RefListNotificationStatus.Preparing; + }); + await CurrentUnitOfWork.SaveChangesAsync(); + + // save attachments if specified + if (attachments != null) + { + foreach (var attachmentDto in attachments) + { + var file = await _storedFileRepository.GetAsync(attachmentDto.StoredFileId); + + await SaveOrUpdateEntityAsync(null, item => + { + item.Message = message; + item.File = file; + item.FileName = attachmentDto.FileName; + }); + } + } - private async Task SendEmailAsync(Guid notificationTemplate, string emailAddress, TData data, GenericEntityReference sourceEntity = null, List attachments = null, string cc = "" ) where TData : NotificationData - { - Guid? messageId = Guid.Empty; + await CurrentUnitOfWork.SaveChangesAsync(); - using (_notificationPublicationContext.BeginScope()) - { - await PublishEmailNotificationAsync( - templateId: notificationTemplate, - data: data, - attachments: attachments, - emailAddress: emailAddress, - sourceEntity: sourceEntity, - cc: cc); - - messageId = _notificationPublicationContext.Statistics.NotificationMessages.FirstOrDefault()?.Id; - } + var sender = new NotificationSender(senderChannelInterface); - return messageId; - } + if (type.IsTimeSensitive) + { + await sender.SendAsync(fromPerson, toPerson, message, true); + } + else + { + await _backgroundJobManager.EnqueueAsync(new DirectNotificationJobArgs() + { + SenderTypeName = channelConfig.SenderTypeName, + FromPerson = fromPerson.Id, + ToPerson = toPerson.Id, + Message = message.Id + }); + } - private async Task SendEmailAsync(Guid notificationTemplate, Person recipient, TData data, GenericEntityReference sourceEntity = null, List attachments = null, string cc = "") where TData : NotificationData - { - Guid? messageId = Guid.Empty; - using (_notificationPublicationContext.BeginScope()) - { - await PublishEmailNotificationAsync( - templateId: notificationTemplate, - data: data, - attachments: attachments, - sourceEntity: sourceEntity, - recipient: recipient, - cc: cc); - - messageId = _notificationPublicationContext.Statistics.NotificationMessages.FirstOrDefault()?.Id; + await CurrentUnitOfWork.SaveChangesAsync(); } - - return messageId; } - #endregion -} \ No newline at end of file +} diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationPublicationContext.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationPublicationContext.cs deleted file mode 100644 index 87af8f0f0d..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationPublicationContext.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Abp.Dependency; -using Abp.Runtime; -using Shesha.NotificationMessages.Dto; -using System; -using System.Collections.Generic; - -namespace Shesha.Notifications -{ - /// - /// Notification publication context. Is used to pass parameters and collect statistics - /// - public class NotificationPublicationContext : INotificationPublicationContext, ITransientDependency - { - private const string ScopeKey = "sha-notification-publication"; - private readonly IAmbientScopeProvider _scopeProvider; - - public NotificationPublicationContext(IAmbientScopeProvider scopeProvider) - { - _scopeProvider = scopeProvider; - } - - /// inheritedDoc - public IDisposable BeginScope() - { - var state = new NotificationPublicationStatistics(); - - return _scopeProvider.BeginScope(ScopeKey, state); - } - - /// inheritedDoc - public NotificationPublicationStatistics Statistics - { - get - { - return _scopeProvider.GetValue(ScopeKey); - } - } - - /// inheritedDoc - public void NotificationMessageCreated(NotificationMessageDto notificationMessageDto) - { - var statistics = Statistics; - if (statistics != null) - statistics.NotificationMessages.Add(notificationMessageDto); - } - } - - public class NotificationPublicationStatistics - { - /// - /// List of created notification messages - /// - public List NotificationMessages { get; private set; } = new List(); - } - - /// - /// Notification publication context. Is used to pass parameters and collect statistics - /// - public interface INotificationPublicationContext - { - /// - /// Begin new scope. All statistics will be collected as part of this scope - /// - /// - IDisposable BeginScope(); - - /// - /// Statistics - /// - NotificationPublicationStatistics Statistics { get; } - - /// - /// Register createtion of notification message - /// - /// - void NotificationMessageCreated(NotificationMessageDto notificationMessageDto); - } -} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs similarity index 52% rename from shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs rename to shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs index 7369f77f08..26ce0b00eb 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/NotificationSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs @@ -11,7 +11,6 @@ using Shesha.Domain.Enums; using Shesha.Email.Dtos; using Shesha.NHibernate; -using Shesha.Notifications.Dto; using Shesha.Services; using System; using System.Collections.Generic; @@ -19,7 +18,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications +namespace Shesha.Notifications { public class NotificationSender: INotificationSender { @@ -32,12 +31,12 @@ public NotificationSender(INotificationChannelSender channelSender) _channelSender = channelSender; } - private async Task> GetAttachmentsAsync(OmoNotificationMessage message) + private async Task> GetAttachmentsAsync(NotificationMessage message) { - var _attachmentRepository = StaticContext.IocManager.Resolve>(); + var _attachmentRepository = StaticContext.IocManager.Resolve>(); var _fileService = StaticContext.IocManager.Resolve(); - var attachments = await _attachmentRepository.GetAll().Where(a => a.PartOf.Id == message.Id).ToListAsync(); + var attachments = await _attachmentRepository.GetAll().Where(a => a.Message.Id == message.Id).ToListAsync(); var result = attachments.Select(a => new EmailAttachment(a.FileName, _fileService.GetStream(a.File))).ToList(); @@ -45,83 +44,9 @@ private async Task> GetAttachmentsAsync(OmoNotificationMes } [UnitOfWork] - public async Task SendBroadcastAsync(OmoNotification notification, string subject, string message, List attachments) + public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml) { - //int attempt = 0; - //bool sentSuccessfully = false; - //var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); - - await _channelSender.BroadcastAsync(notification.NotificationTopic, subject, message, attachments); - - //var messages = _notificationMessageRepository.GetAll().Where(m => m.PartOf.Id == notification.Id).ToList(); - - //foreach (var message in messages) - //{ - // var fromPerson = message.FromPerson; - // var toPerson = message.ToPerson; - - // while (attempt < MaxRetries && !sentSuccessfully) - // { - // try - // { - // // Attempt to send the message - // sentSuccessfully = _channelSender.Send(fromPerson, toPerson, message, true, "", true, await GetAttachmentsAsync(message)); - - // if (sentSuccessfully) - // { - // // Successful send, exit the loop - // message.Status = RefListNotificationStatus.Sent; - - // break; - // } - // else - // { - // Console.WriteLine($"Attempt {attempt + 1} to send notification failed."); - // } - // } - // catch (Exception ex) - // { - // message.ErrorMessage = $"Exception on attempt {attempt + 1} to send notification: {ex.Message}"; - // Console.WriteLine($"Exception on attempt {attempt + 1} to send notification: {ex.Message}"); - // } - - // // Increment retry count and save to the database - // attempt++; - // message.RetryCount = attempt; - // message.ErrorMessage = $"Failed to send notification after {attempt} attempts."; - // message.Status = RefListNotificationStatus.Failed; - - // using (var uow = StaticContext.IocManager.Resolve().Begin()) - // { - // using (var transaction = StaticContext.IocManager.Resolve().Session.BeginTransaction()) - // { - // await _notificationMessageRepository.UpdateAsync(message); - // transaction.Commit(); - // } - // await uow.CompleteAsync(); - // } - - // // Introduce a delay before the next retry attempt - // if (attempt < MaxRetries) - // { - // await Task.Delay(1000); // Backoff delay - // } - // } - - // if (!sentSuccessfully) - // { - // Console.WriteLine($"Failed to send notification after {MaxRetries} attempts."); - // //throw new Exception("Failed to send notification after maximum retry attempts."); - // } - //} - - } - - - [UnitOfWork] - public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml) - { - var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); + var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); var _unitOfWorkManager = StaticContext.IocManager.Resolve(); var _sessionProvider = StaticContext.IocManager.Resolve(); var attachments = await GetAttachmentsAsync(message); @@ -184,5 +109,76 @@ public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationM } } + + [UnitOfWork] + public async Task SendBroadcastAsync(Notification notification, string subject, string messageContent, List attachments) + { + int attempt = 0; + bool sentSuccessfully = false; + var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); + + // Get all notification messages associated with the notification + var messages = _notificationMessageRepository.GetAll().Where(m => m.PartOf.Id == notification.Id).ToList(); + + while (attempt < MaxRetries && !sentSuccessfully) + { + try + { + // Attempt to send the message via the channel sender + sentSuccessfully = await _channelSender.BroadcastAsync(notification.NotificationTopic, subject, messageContent, attachments); + + foreach (var message in messages) + { + if (sentSuccessfully) + { + // Update the status to Sent for successful messages + message.Status = RefListNotificationStatus.Sent; + message.RetryCount = attempt; + message.ErrorMessage = null; // Clear error message + } + else + { + Console.WriteLine($"Attempt {attempt + 1} to send notification failed."); + message.Status = RefListNotificationStatus.Failed; + message.RetryCount = attempt; + message.ErrorMessage = $"Failed to send notification on attempt {attempt + 1}."; + } + + // Save changes to each message + using (var uow = StaticContext.IocManager.Resolve().Begin()) + { + using (var transaction = StaticContext.IocManager.Resolve().Session.BeginTransaction()) + { + await _notificationMessageRepository.UpdateAsync(message); + transaction.Commit(); + } + await uow.CompleteAsync(); + } + } + + if (sentSuccessfully) + { + // If any message was successfully sent, exit the loop + break; + } + } + catch (Exception ex) + { + Console.WriteLine($"Exception on attempt {attempt + 1} to send broadcast notification: {ex.Message}"); + } + + // Introduce a delay before retrying + attempt++; + if (attempt < MaxRetries) + { + await Task.Delay(1000); // Backoff delay + } + } + + if (!sentSuccessfully) + { + Console.WriteLine($"Failed to send notification after {MaxRetries} attempts."); + } + } } } diff --git a/shesha-core/src/Shesha.Application/Notifications/RealTimeNotifierBase.cs b/shesha-core/src/Shesha.Application/Notifications/RealTimeNotifierBase.cs deleted file mode 100644 index 1673968342..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/RealTimeNotifierBase.cs +++ /dev/null @@ -1,557 +0,0 @@ -using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Abp.Notifications; -using Hangfire; -using Shesha.Authorization.Users; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.EntityReferences; -using Shesha.Exceptions; -using Shesha.Extensions; -using Shesha.NHibernate; -using Shesha.NotificationMessages.Dto; -using Shesha.Notifications.Dto; -using Shesha.Notifications.Exceptions; -using Shesha.Utilities; -using Stubble.Core; -using Stubble.Core.Builders; -using Stubble.Core.Settings; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Transactions; - -namespace Shesha.Notifications -{ - /// - /// Base class of realtime notifier. Key features: - /// 1. Generation of messages based on templates. Uses mustache syntax - /// 2. Audit (uses ) - /// 3. Work in background with retries - /// - public abstract class RealTimeNotifierBase - { - protected readonly UserManager UserManager; - protected readonly IRepository PersonRepository; - protected readonly IRepository NotificationRepository; - protected readonly IRepository TemplateRepository; - protected readonly IRepository NotificationMessageRepository; - - public abstract RefListNotificationType NotificationType { get; } - - private readonly StubbleVisitorRenderer _stubbleRenderer = new StubbleBuilder().Configure(settings => - { - //settings.di - }).Build(); // todo: move to DI Container - - protected readonly IUnitOfWorkManager UowManager; - - protected RealTimeNotifierBase(UserManager userManager, IRepository personRepository, - IRepository templateRepository, - IRepository notificationMessageRepository, IUnitOfWorkManager uowManager, - IRepository notificationRepository) - { - UserManager = userManager; - PersonRepository = personRepository; - TemplateRepository = templateRepository; - NotificationMessageRepository = notificationMessageRepository; - UowManager = uowManager; - NotificationRepository = notificationRepository; - } - - private IQueryable GetTemplateQuery(string notificationName, - RefListNotificationType sendType) - { - return TemplateRepository.GetAll() - .Where(t => t.Notification != null && - t.Notification.Name == notificationName && - t.SendType == sendType) - .OrderByDescending(t => t.CreationTime); - } - - - /// - /// Get template by notification name and send type (email/sms etc) - /// - protected async Task GetTemplateAsync(string notificationName, - RefListNotificationType sendType) - { - return await GetTemplateQuery(notificationName, sendType).FirstOrDefaultAsync(); - } - - /// - /// Get template by notification name and send type (email/sms etc) - /// - protected NotificationTemplate GetTemplate(string notificationName, RefListNotificationType sendType) - { - return GetTemplateQuery(notificationName, sendType).FirstOrDefault(); - } - - private IQueryable GetNotificationQuery(string notificationName) - { - return NotificationRepository.GetAll().Where(t => t.Name == notificationName); - } - - /// - /// Get notification by name - /// - protected Notification GetNotification(string notificationName) - { - return GetNotificationQuery(notificationName).FirstOrDefault(); - } - - /// - /// Get notification by name - /// - protected async Task GetNotificationAsync(string notificationName) - { - return await GetNotificationQuery(notificationName).FirstOrDefaultAsync(); - } - - private IQueryable GetRecipientQuery(UserNotification notification) - { - return PersonRepository.GetAll().Where(p => p.User.Id == notification.UserId); - } - - /// - /// Get recipient (person) of the message - /// - protected async Task GetRecipientAsync(UserNotification notification) - { - return await GetRecipientQuery(notification).FirstOrDefaultAsync(); - } - - /// - /// Get recipient (person) of the message - /// - protected Person GetRecipient(UserNotification notification) - { - return GetRecipientQuery(notification).FirstOrDefault(); - } - - /// - /// Generate content based on template (uses mustache syntax) - /// - protected async Task GenerateContentAsync(string template, UserNotification notification) - { - return !string.IsNullOrWhiteSpace(template) - ? await _stubbleRenderer.RenderAsync(template, notification.Notification.Data) - : template; - } - - /// - /// Generate content based on template (uses mustache syntax) - /// - protected string GenerateContent(string template, NotificationData data, bool stripHtml, bool removeDiacritics = false) - { - if (removeDiacritics) - { - // remove sms-unsupported symbols - var props = data.GetType().GetProperties(); - foreach (var prop in props) - { - if (prop.PropertyType == typeof(string) && prop.CanWrite) - { - prop.SetValue(data, prop.GetValue(data)?.ToString().RemoveDiacritics()); - } - } - } - - var result = !string.IsNullOrWhiteSpace(template) - ? removeDiacritics - ? _stubbleRenderer.Render(template, data, - data.Properties.ToDictionary(i => i.Key, i => i.Value.ToString()), new RenderSettings() { SkipHtmlEncoding = true }) - : _stubbleRenderer.Render(template, data, - data.Properties.ToDictionary(i => i.Key, i => i.Value.ToString())) - : template; - - if (stripHtml) - result = result?.StripHtml()?.Trim(); - - return result; - } - - /// - /// Create new record in the . Is used for audit purposes and retries - /// - /// Id of the template - /// Id of the person - /// fill data action (specific to the send type) - /// - protected async Task CreateNotificationMessageAsync(Guid templateId, Guid? recipientId, - Action fillData) - { - var messageId = Guid.NewGuid(); - using (var uow = UowManager.Begin(TransactionScopeOption.RequiresNew)) - { - // get template again to isolate sessions - var localTemplate = await TemplateRepository.GetAsync(templateId); - var localRecipient = recipientId.HasValue - ? await PersonRepository.GetAsync(recipientId.Value) - : null; - var message = new NotificationMessage() - { - Id = messageId, - Notification = localTemplate.Notification, - Template = localTemplate, - Recipient = localRecipient, - Status = RefListNotificationStatus.Outgoing, - TryCount = 0 - }; - - fillData.Invoke(message); - - await NotificationMessageRepository.InsertAsync(message); - await uow.CompleteAsync(); - } - - return messageId; - } - - /// - /// Create new record in the . Is used for audit purposes and retries - /// - /// Id of the template - /// Id of the person - /// fill data action (specific to the send type) - /// - protected Guid CreateNotificationMessage(Guid templateId, Guid? recipientId, - Action fillData) - { - var messageId = Guid.NewGuid(); - using (var uow = UowManager.Begin(TransactionScopeOption.RequiresNew)) - { - // get template again to isolate sessions - var localTemplate = TemplateRepository.Get(templateId); - var localRecipient = recipientId.HasValue - ? PersonRepository.Get(recipientId.Value) - : null; - var message = new NotificationMessage() - { - Id = messageId, - Notification = localTemplate.Notification, - Template = localTemplate, - Recipient = localRecipient, - Status = RefListNotificationStatus.Outgoing, - TryCount = 0 - }; - - fillData.Invoke(message); - - NotificationMessageRepository.Insert(message); - uow.Complete(); - } - - return messageId; - } - - /// - /// Update Notification Message - /// - protected async Task UpdateNotificationMessageAsync(Guid id, Action fillData) - { - using (var uow = UowManager.Begin(TransactionScopeOption.RequiresNew)) - { - var message = await NotificationMessageRepository.GetAsync(id); - - fillData.Invoke(message); - - await NotificationMessageRepository.UpdateAsync(message); - await uow.CompleteAsync(); - } - } - - /// - /// Update Notification Message - /// - protected void UpdateNotificationMessage(Guid id, Action fillData) - { - using (var uow = UowManager.Begin(TransactionScopeOption.RequiresNew)) - { - var message = NotificationMessageRepository.Get(id); - - fillData.Invoke(message); - - NotificationMessageRepository.Update(message); - uow.Complete(); - } - } - - /// - /// Sends notification message. Must be implemented in the derived class - /// - /// - /// - protected abstract Task DoSendNotificationMessageAsync(NotificationMessage message); - - /// - /// Sends notification message. Must be implemented in the derived class - /// - /// - /// - protected abstract void DoSendNotificationMessage(NotificationMessage message); - - /// - /// Send notification in the background task. Used by Hangfire - /// - /// - /// - [AutomaticRetry(Attempts = 5, DelaysInSeconds = new int[] { 10, 20, 20, 20, 20 })] - public async Task SendNotification(Guid id) - { - RefListNotificationStatus? status = null; - using (var uow = UowManager.Begin(TransactionScopeOption.RequiresNew)) - { - // safe get message and skip if deleted manually - var message = await NotificationMessageRepository.GetAsync(id); - - if (message == null) - throw new ShaNotificationNotFoundException(id); - - if (message.Status == RefListNotificationStatus.Preparing) - throw new ShaNotificationIsStillPreparingException(id); - - message.TryCount++; - message.SendDate = DateTime.Now; - try - { - await DoSendNotificationMessageAsync(message); - - message.ErrorMessage = null; - message.Status = RefListNotificationStatus.Sent; - await NotificationMessageRepository.UpdateAsync(message); - } - catch (Exception e) - { - // update message - message.ErrorMessage = e.FullMessage(); - message.Status = message.TryCount < 5 - ? RefListNotificationStatus.WaitToRetry - : RefListNotificationStatus.Failed; - await NotificationMessageRepository.UpdateAsync(message); - } - - status = message.Status; - await uow.CompleteAsync(); - } - - // throw exception if we need to retry - if (status == RefListNotificationStatus.WaitToRetry) - throw new ShaNotificationFailedWaitRetryException(id); - } - - private async Task SendNotificationInternalAsync(NotificationMessage message) - { - if (message == null) - return; - - if (message.Status == RefListNotificationStatus.Preparing) - return; - - message.TryCount++; - message.SendDate = DateTime.Now; - try - { - await DoSendNotificationMessageAsync(message); - - message.ErrorMessage = null; - message.Status = RefListNotificationStatus.Sent; - await NotificationMessageRepository.UpdateAsync(message); - } - catch (Exception e) - { - // update message - message.ErrorMessage = e.FullMessage(); - message.Status = message.TryCount < 5 - ? RefListNotificationStatus.WaitToRetry - : RefListNotificationStatus.Failed; - await NotificationMessageRepository.UpdateAsync(message); - } - } - - private void SendNotificationInternal(NotificationMessage message) - { - if (message == null) - return; - - if (message.Status == RefListNotificationStatus.Preparing) - return; - - message.TryCount++; - message.SendDate = DateTime.Now; - try - { - DoSendNotificationMessage(message); - - message.ErrorMessage = null; - message.Status = RefListNotificationStatus.Sent; - NotificationMessageRepository.Update(message); - } - catch (Exception e) - { - // update message - message.ErrorMessage = e.FullMessage(); - message.Status = message.TryCount < 5 - ? RefListNotificationStatus.WaitToRetry - : RefListNotificationStatus.Failed; - NotificationMessageRepository.Update(message); - } - } - - - - /// - /// Return notification data (unwrap if needed) - /// - /// - protected NotificationData GetNotificationData(TenantNotificationInfo tenantNotificationInfo) - { - var tenantNotification = tenantNotificationInfo.ToTenantNotification(); - return tenantNotification.Data is ShaNotificationData shaNotificationData - ? shaNotificationData.GetData() - : tenantNotification.Data; - } - - /// - /// Send template-based notifications - /// - public void SendNotifications(List notificationMessages, - RefListNotificationType notificationType) - { - var messageDtos = notificationMessages.Where(n => - n.SendType?.ItemValue == (int) notificationType && - n.Status?.ItemValue == (int) RefListNotificationStatus.Preparing && - !string.IsNullOrWhiteSpace(n.RecipientText)).ToList(); - - if (!messageDtos.Any()) - return; - - using (var uow = UowManager.Begin()) - { - foreach (var messageDto in messageDtos) - { - var message = NotificationMessageRepository.Get(messageDto.Id); - if (message.TenantNotification == null) - throw new Exception($"{message.TenantNotification} must not be null"); - - var tenantNotification = message.TenantNotification.ToTenantNotification(); - if (tenantNotification.Data is ShaNotificationData shaNotificationData) - { - var template = shaNotificationData.TemplateId.HasValue - ? TemplateRepository.Get(shaNotificationData.TemplateId.Value) - : GetTemplate(message.TenantNotification.NotificationName, notificationType); - - if (template == null || !template.IsEnabled) - continue; - - if (template.SendType != notificationType) - throw new Exception( - $"Wrong type of template. Expected `{notificationType}`, actual `{template.SendType}`"); - - var notificationData = shaNotificationData.GetData(); - - message.Notification = template.Notification; - message.Template = template; - message.Subject = GenerateContent(template.Subject, notificationData, true, message.SendType == RefListNotificationType.SMS); - message.Body = GenerateContent(template.Body, notificationData, - template.BodyFormat == RefListNotificationTemplateType.PlainText, - message.SendType == RefListNotificationType.SMS); - message.Status = RefListNotificationStatus.Outgoing; - if (!string.IsNullOrEmpty(shaNotificationData.SourceEntityId)) - message.SourceEntity = new GenericEntityReference(shaNotificationData.SourceEntityId, shaNotificationData.SourceEntityClassName, shaNotificationData.SourceEntityDisplayName); - NotificationMessageRepository.Update(message); - - // schedule sending - SendNotificationInternal(message); - } - } - - uow.Complete(); - } - } - - /// - /// Send template-based notifications - /// - public async Task SendNotificationsAsync(List notificationMessages, - RefListNotificationType notificationType) - { - var messageDtos = notificationMessages.Where(n => - n.SendType?.ItemValue == (int) notificationType && - n.Status?.ItemValue == (int) RefListNotificationStatus.Preparing && - !string.IsNullOrWhiteSpace(n.RecipientText)).ToList(); - - if (!messageDtos.Any()) - return; - - var messagesToSend = new List(); - using (var uow = UowManager.Begin()) - { - foreach (var messageDto in messageDtos) - { - var message = await NotificationMessageRepository.GetAsync(messageDto.Id); - if (message.TenantNotification == null) - throw new Exception($"{message.TenantNotification} must not be null"); - - var tenantNotification = message.TenantNotification.ToTenantNotification(); - if (tenantNotification.Data is ShaNotificationData shaNotificationData) - { - var template = shaNotificationData.TemplateId.HasValue - ? await TemplateRepository.GetAsync(shaNotificationData.TemplateId.Value) - : await GetTemplateAsync(message.TenantNotification.NotificationName, notificationType); - - if (template == null || !template.IsEnabled) - continue; - - if (template.SendType != notificationType) - throw new Exception( - $"Wrong type of template. Expected `{notificationType}`, actual `{template.SendType}`"); - - var notificationData = shaNotificationData.GetData(); - - message.Notification = template.Notification; - message.Template = template; - message.Cc = shaNotificationData.Cc; - message.Subject = GenerateContent(template.Subject, notificationData, true); - message.Body = GenerateContent(template.Body, notificationData, - template.BodyFormat == RefListNotificationTemplateType.PlainText, - message.SendType == RefListNotificationType.SMS); - message.Status = RefListNotificationStatus.Outgoing; - if (!string.IsNullOrEmpty(shaNotificationData.SourceEntityId)) - message.SourceEntity = new GenericEntityReference(shaNotificationData.SourceEntityId, shaNotificationData.SourceEntityClassName, shaNotificationData.SourceEntityDisplayName); ; - await NotificationMessageRepository.UpdateAsync(message); - - messagesToSend.Add(message.Id); - } - } - - // send all prepared messages in background - UowManager.Current.DoAfterTransaction(() => - { - foreach (var messageId in messagesToSend) - { - BackgroundJob.Enqueue(() => SendNotification(messageId)); - } - }); - - await uow.CompleteAsync(); - } - } - - /// - /// Re-sends specified notification message - /// - /// - /// - public async Task ResendMessageAsync(NotificationMessageDto notificationMessage) - { - if (notificationMessage.SendType == null || - notificationMessage.SendType.ItemValue != (int) NotificationType) - return; - - var message = await NotificationMessageRepository.GetAsync(notificationMessage.Id); - await SendNotificationInternalAsync(message); - } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/ShaNotificationDistributer.cs b/shesha-core/src/Shesha.Application/Notifications/ShaNotificationDistributer.cs deleted file mode 100644 index dace089a4a..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/ShaNotificationDistributer.cs +++ /dev/null @@ -1,367 +0,0 @@ -using Abp; -using Abp.Collections.Extensions; -using Abp.Configuration; -using Abp.Dependency; -using Abp.Domain.Repositories; -using Abp.Domain.Services; -using Abp.Domain.Uow; -using Abp.Extensions; -using Abp.Notifications; -using Abp.Runtime.Session; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.EntityReferences; -using Shesha.NotificationMessages.Dto; -using Shesha.Notifications.Dto; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Shesha.Notifications -{ - public class ShaNotificationDistributer : DomainService, IShaNotificationDistributer - { - private readonly INotificationConfiguration _notificationConfiguration; - private readonly INotificationDefinitionManager _notificationDefinitionManager; - private readonly INotificationStore _notificationStore; - private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly IGuidGenerator _guidGenerator; - private readonly IIocResolver _iocResolver; - private readonly IRepository _messageRepository; - private readonly IRepository _storedFileRepository; - private readonly IRepository _attachmentRepository; - private readonly INotificationPublicationContext _publicationContext; - private readonly IRepository _personRepository; - - public IAbpSession AbpSession { get; set; } = NullAbpSession.Instance; - - /// - /// Initializes a new instance of the class. - /// - public ShaNotificationDistributer( - INotificationConfiguration notificationConfiguration, - INotificationDefinitionManager notificationDefinitionManager, - INotificationStore notificationStore, - IUnitOfWorkManager unitOfWorkManager, - IGuidGenerator guidGenerator, - IIocResolver iocResolver, - IRepository messageRepository, - IRepository storedFileRepository, - IRepository attachmentRepository, - INotificationPublicationContext publicationContext, - IRepository personRepository) - { - _notificationConfiguration = notificationConfiguration; - _notificationDefinitionManager = notificationDefinitionManager; - _notificationStore = notificationStore; - _unitOfWorkManager = unitOfWorkManager; - _guidGenerator = guidGenerator; - _iocResolver = iocResolver; - _messageRepository = messageRepository; - _storedFileRepository = storedFileRepository; - _attachmentRepository = attachmentRepository; - _publicationContext = publicationContext; - _personRepository = personRepository; - } - - public async Task DistributeAsync(Guid notificationId) - { - var notificationInfo = await _notificationStore.GetNotificationOrNullAsync(notificationId); - if (notificationInfo == null) - { - Logger.Warn("NotificationDistributionJob can not continue since could not found notification by id: " + notificationId); - return; - } - - var users = await GetUsersAsync(notificationInfo); - if (users.Any()) - { - var userNotifications = await SaveUserNotificationsAsync(users, notificationInfo); - - await _notificationStore.DeleteNotificationAsync(notificationInfo); - - await NotifyAsync(userNotifications.ToArray()); - } - else - { - // handle Shesha messages without recipients - // todo: combine with subscriptions - var notificationMessages = await SaveUserNotificationMessagesAsync(notificationInfo); - await NotifyAsync(notificationMessages); - } - } - - /// inheritedDoc - public async Task ResendMessageAsync(NotificationMessageDto notificationMessage) - { - var notifierTypes = _notificationConfiguration.Notifiers - .Where(t => typeof(IShaRealTimeNotifier).IsAssignableFrom(t)).ToList(); - foreach (var notifierType in notifierTypes) - { - try - { - using (var notifier = _iocResolver.ResolveAsDisposable(notifierType)) - { - if ((int)notifier.Object.NotificationType == notificationMessage.SendType?.ItemValue) - await notifier.Object.ResendMessageAsync(notificationMessage); - } - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), ex); - } - } - } - - [UnitOfWork] - protected virtual async Task GetUsersAsync(NotificationInfo notificationInfo) - { - List userIds; - - if (!notificationInfo.UserIds.IsNullOrEmpty()) - { - //Directly get from UserIds - userIds = notificationInfo - .UserIds - .Split(",") - .Select(uidAsStr => UserIdentifier.Parse(uidAsStr)) - .Where(uid => SettingManager.GetSettingValueForUser(NotificationSettingNames.ReceiveNotifications, uid.TenantId, uid.UserId)) - .ToList(); - } - else - { - //Get subscribed users - - var tenantIds = GetTenantIds(notificationInfo); - - List subscriptions; - - if (tenantIds.IsNullOrEmpty() || - (tenantIds.Length == 1 && tenantIds[0] == NotificationInfo.AllTenantIds.To())) - { - //Get all subscribed users of all tenants - subscriptions = await _notificationStore.GetSubscriptionsAsync( - notificationInfo.NotificationName, - notificationInfo.EntityTypeName, - notificationInfo.EntityId, - notificationInfo.TargetNotifiers - ); - } - else - { - //Get all subscribed users of specified tenant(s) - subscriptions = await _notificationStore.GetSubscriptionsAsync( - tenantIds, - notificationInfo.NotificationName, - notificationInfo.EntityTypeName, - notificationInfo.EntityId, - notificationInfo.TargetNotifiers - ); - } - - //Remove invalid subscriptions - var invalidSubscriptions = new Dictionary(); - - //TODO: Group subscriptions per tenant for potential performance improvement - foreach (var subscription in subscriptions) - { - using (CurrentUnitOfWork.SetTenantId(subscription.TenantId)) - { - if (!await _notificationDefinitionManager.IsAvailableAsync(notificationInfo.NotificationName, new UserIdentifier(subscription.TenantId, subscription.UserId)) || - !SettingManager.GetSettingValueForUser(NotificationSettingNames.ReceiveNotifications, subscription.TenantId, subscription.UserId)) - { - invalidSubscriptions[subscription.Id] = subscription; - } - } - } - - subscriptions.RemoveAll(s => invalidSubscriptions.ContainsKey(s.Id)); - - //Get user ids - userIds = subscriptions - .Select(s => new UserIdentifier(s.TenantId, s.UserId)) - .ToList(); - } - - if (!notificationInfo.ExcludedUserIds.IsNullOrEmpty()) - { - //Exclude specified users. - var excludedUserIds = notificationInfo - .ExcludedUserIds - .Split(",") - .Select(uidAsStr => UserIdentifier.Parse(uidAsStr)) - .ToList(); - - userIds.RemoveAll(uid => excludedUserIds.Any(euid => euid.Equals(uid))); - } - - return userIds.ToArray(); - } - - private static int?[] GetTenantIds(NotificationInfo notificationInfo) - { - if (notificationInfo.TenantIds.IsNullOrEmpty()) - { - return new int?[]{ null }; - } - - return notificationInfo - .TenantIds - .Split(",") - .Select(tenantIdAsStr => tenantIdAsStr == "null" ? (int?)null : (int?)tenantIdAsStr.To()) - .ToArray(); - } - - [UnitOfWork] - protected virtual async Task> SaveUserNotificationsAsync(UserIdentifier[] users, NotificationInfo notificationInfo) - { - var userNotifications = new List(); - - var tenantGroups = users.GroupBy(user => user.TenantId); - foreach (var tenantGroup in tenantGroups) - { - using (_unitOfWorkManager.Current.SetTenantId(tenantGroup.Key)) - { - var tenantNotificationInfo = new TenantNotificationInfo(_guidGenerator.Create(), tenantGroup.Key, notificationInfo); - await _notificationStore.InsertTenantNotificationAsync(tenantNotificationInfo); - await _unitOfWorkManager.Current.SaveChangesAsync(); //To get tenantNotification.Id. - - var tenantNotification = tenantNotificationInfo.ToTenantNotification(); - - foreach (var user in tenantGroup) - { - var userNotification = new UserNotificationInfo(_guidGenerator.Create()) - { - TenantId = tenantGroup.Key, - UserId = user.UserId, - TenantNotificationId = tenantNotificationInfo.Id - }; - - await _notificationStore.InsertUserNotificationAsync(userNotification); - userNotifications.Add(userNotification.ToUserNotification(tenantNotification)); - } - - await CurrentUnitOfWork.SaveChangesAsync(); //To get Ids of the notifications - } - } - - return userNotifications; - } - - [UnitOfWork] - protected virtual async Task> SaveUserNotificationMessagesAsync(NotificationInfo notificationInfo) - { - var notificationMessages = new List(); - - var tenantIds = GetTenantIds(notificationInfo); - foreach (var tenantId in tenantIds) - { - using (_unitOfWorkManager.Current.SetTenantId(tenantId)) - { - var tenantNotificationInfo = new TenantNotificationInfo(_guidGenerator.Create(), tenantId, notificationInfo); - await _notificationStore.InsertTenantNotificationAsync(tenantNotificationInfo); - await _unitOfWorkManager.Current.SaveChangesAsync(); //To get tenantNotification.Id. - - var tenantNotification = tenantNotificationInfo.ToTenantNotification(); - - if (tenantNotification.Data is ShaNotificationData shaData) - { - await _notificationStore.InsertTenantNotificationAsync(tenantNotificationInfo); - await _unitOfWorkManager.Current.SaveChangesAsync(); //To get tenantNotification.Id. - var notificationMessage = new NotificationMessage - { - TenantNotification = tenantNotificationInfo, - SendType = shaData.SendType, - RecipientText = shaData.RecipientText, - Status = RefListNotificationStatus.Preparing, - }; - if (!string.IsNullOrEmpty(notificationInfo.EntityId) && !string.IsNullOrEmpty(notificationInfo.EntityTypeName)) - notificationMessage.SourceEntity = new GenericEntityReference(notificationInfo.EntityId.Replace('"', ' ').Trim(), notificationInfo.EntityTypeName, ""); - - if (shaData.RecipientId.HasValue) - { - var recipient = await _personRepository.GetAsync(shaData.RecipientId.Value); - if(recipient != null) - notificationMessage.Recipient = recipient; - } - - await _messageRepository.InsertAsync(notificationMessage); - - var notificationMessageDto = ObjectMapper.Map(notificationMessage); - - // save attachments if specified - if (shaData.Attachments != null) - { - foreach (var attachmentDto in shaData.Attachments) - { - var file = await _storedFileRepository.GetAsync(attachmentDto.StoredFileId); - var attachment = new NotificationMessageAttachment - { - Message = notificationMessage, - File = file, - FileName = attachmentDto.FileName - }; - await _attachmentRepository.InsertAsync(attachment); - - notificationMessageDto.Attachments.Add(ObjectMapper.Map(attachment)); - } - } - - notificationMessages.Add(notificationMessageDto); - - await CurrentUnitOfWork.SaveChangesAsync(); //To get Ids of the notifications - - // collect statistics - _publicationContext.NotificationMessageCreated(notificationMessageDto); - } - } - } - - return notificationMessages; - } - - #region Protected methods - - protected virtual async Task NotifyAsync(UserNotification[] userNotifications) - { - foreach (var notifierType in _notificationConfiguration.Notifiers) - { - try - { - using (var notifier = _iocResolver.ResolveAsDisposable(notifierType)) - { - await notifier.Object.SendNotificationsAsync(userNotifications); - } - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), ex); - } - } - } - - protected virtual async Task NotifyAsync(List notificationMessages) - { - var shaNotifierTypes = _notificationConfiguration.Notifiers - .Where(t => typeof(IShaRealTimeNotifier).IsAssignableFrom(t)).ToList(); - - foreach (var notifierType in shaNotifierTypes) - { - try - { - using (var notifier = _iocResolver.ResolveAsDisposable(notifierType)) - { - await notifier.Object.SendNotificationsAsync(notificationMessages); - } - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), ex); - } - } - } - - #endregion - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/ShaNotificationProvider.cs b/shesha-core/src/Shesha.Application/Notifications/ShaNotificationProvider.cs deleted file mode 100644 index fc3ee8710c..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/ShaNotificationProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Abp.Authorization; -using Abp.Localization; -using Abp.Notifications; - -namespace Shesha.Notifications -{ - public class ShaNotificationProvider: NotificationProvider - { - public override void SetNotifications(INotificationDefinitionContext context) - { - context.Manager.Add( - new NotificationDefinition( - "ShaMyCustomEvent", - displayName: new LocalizableString("NewUserRegisteredNotificationDefinition", "MyLocalizationSourceName"), - permissionDependency: null //new SimplePermissionDependency("App.Pages.UserManagement") - ) - ); - } - } -} diff --git a/shesha-core/src/Shesha.Application/Notifications/ShaNotificationPublisher.cs b/shesha-core/src/Shesha.Application/Notifications/ShaNotificationPublisher.cs deleted file mode 100644 index 5a701a4956..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/ShaNotificationPublisher.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Abp; -using Abp.BackgroundJobs; -using Abp.Collections.Extensions; -using Abp.Dependency; -using Abp.Domain.Entities; -using Abp.Domain.Uow; -using Abp.Extensions; -using Abp.Json; -using Abp.Notifications; -using Abp.Runtime.Session; -using Shesha.Notifications.Dto; -using Shesha.Notifications.Exceptions; -using System; -using System.Linq; -using System.Threading.Tasks; -using System.Transactions; - -namespace Shesha.Notifications -{ - /// - /// Implements . - /// - public class ShaNotificationPublisher : AbpServiceBase, INotificationPublisher, ITransientDependency - { - public const int MaxUserCountToDirectlyDistributeANotification = 5; - - private readonly IShaNotificationDistributer _notificationDistributer; - - /// - /// Indicates all tenants. - /// - public static int[] AllTenants - { - get - { - return new[] { NotificationInfo.AllTenantIds.To() }; - } - } - - /// - /// Reference to ABP session. - /// - public IAbpSession AbpSession { get; set; } - - private readonly INotificationStore _store; - private readonly IBackgroundJobManager _backgroundJobManager; - private readonly INotificationConfiguration _notificationConfiguration; - private readonly IGuidGenerator _guidGenerator; - private readonly IIocResolver _iocResolver; - - /// - /// Initializes a new instance of the class. - /// - public ShaNotificationPublisher( - INotificationStore store, - IBackgroundJobManager backgroundJobManager, - INotificationConfiguration notificationConfiguration, - IGuidGenerator guidGenerator, - IIocResolver iocResolver, - IShaNotificationDistributer notificationDistributer) - { - _store = store; - _backgroundJobManager = backgroundJobManager; - _notificationConfiguration = notificationConfiguration; - _guidGenerator = guidGenerator; - _iocResolver = iocResolver; - AbpSession = NullAbpSession.Instance; - _notificationDistributer = notificationDistributer; - } - - //Create EntityIdentifier includes entityType and entityId. - [UnitOfWork] - public virtual async Task PublishAsync( - string notificationName, - NotificationData data = null, - EntityIdentifier entityIdentifier = null, - NotificationSeverity severity = NotificationSeverity.Info, - UserIdentifier[] userIds = null, - UserIdentifier[] excludedUserIds = null, - int?[] tenantIds = null, - Type[] targetNotifiers = null) - { - Guid? notificationId = null; - - using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) - { - if (notificationName.IsNullOrEmpty()) - { - throw new ArgumentException("NotificationName can not be null or whitespace!", "notificationName"); - } - - if (!tenantIds.IsNullOrEmpty() && !userIds.IsNullOrEmpty()) - { - throw new ArgumentException("tenantIds can be set only if userIds is not set!", "tenantIds"); - } - - if (tenantIds.IsNullOrEmpty() && userIds.IsNullOrEmpty()) - { - tenantIds = new[] { AbpSession.TenantId }; - } - - var notificationInfo = new NotificationInfo(_guidGenerator.Create()) - { - NotificationName = notificationName, - EntityTypeName = entityIdentifier == null ? null : entityIdentifier.Type.FullName, - EntityTypeAssemblyQualifiedName = entityIdentifier == null ? null : entityIdentifier.Type.AssemblyQualifiedName, - EntityId = entityIdentifier == null ? null : entityIdentifier.Id.ToJsonString(), - Severity = severity, - UserIds = userIds.IsNullOrEmpty() ? null : userIds.Select(uid => uid.ToUserIdentifierString()).JoinAsString(","), - ExcludedUserIds = excludedUserIds.IsNullOrEmpty() ? null : excludedUserIds.Select(uid => uid.ToUserIdentifierString()).JoinAsString(","), - TenantIds = tenantIds.IsNullOrEmpty() ? null : tenantIds.JoinAsString(","), - Data = data == null ? null : data.ToJsonString(), - DataTypeName = data == null ? null : data.GetType().AssemblyQualifiedName - }; - - await _store.InsertNotificationAsync(notificationInfo); - - await uow.CompleteAsync(); //To get Id of the notification - - notificationId = notificationInfo.Id; - } - - if (notificationId == null) - throw new ShaNotificationSaveFailedException(notificationName, data); - - var isShaNotification = data != null && data is ShaNotificationData; - if (isShaNotification || userIds != null && userIds.Length <= MaxUserCountToDirectlyDistributeANotification) - { - await _notificationDistributer.DistributeAsync(notificationId.Value); - } - else - { - //We enqueue a background job since distributing may get a long time - await _backgroundJobManager.EnqueueAsync( - new NotificationDistributionJobArgs( - notificationId.Value - ) - ); - } - } - } -} diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs similarity index 90% rename from shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs rename to shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs index 8dc0b3aec8..3a25fe5b05 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ClickatellGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs @@ -1,8 +1,8 @@ using Shesha.Domain; using Shesha.Email.Dtos; -using Shesha.OmoNotifications.Configuration.Email; -using Shesha.OmoNotifications.Configuration; -using Shesha.OmoNotifications.Helpers; +using Shesha.Notifications.Configuration.Email; +using Shesha.Notifications.Configuration; +using Shesha.Notifications.Helpers; using System; using System.Collections.Generic; using System.Linq; @@ -10,9 +10,9 @@ using System.Text; using System.Threading.Tasks; using System.Web; -using Shesha.OmoNotifications.Configuration.Sms; +using Shesha.Notifications.Configuration.Sms; -namespace Shesha.OmoNotifications.Sms.Gateways +namespace Shesha.Notifications.Sms.Gateways { public class ClickatellGateway: ISmsGateway { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs similarity index 89% rename from shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs rename to shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs index 0020edb979..aa92672d57 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/ISmsGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Sms.Gateways +namespace Shesha.Notifications.Sms.Gateways { public interface ISmsGateway { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs similarity index 94% rename from shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs rename to shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs index a5030c2aa0..1e33eea75c 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/Gateways/SmsGatewayFactory.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Sms.Gateways +namespace Shesha.Notifications.Sms.Gateways { public class SmsGatewayFactory { diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs similarity index 83% rename from shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs rename to shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs index 1c576825bb..288a0a6659 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Sms/SmsChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs @@ -5,13 +5,12 @@ using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Email.Dtos; -using Shesha.Notifications.Dto; -using Shesha.OmoNotifications.Configuration; -using Shesha.OmoNotifications.Configuration.Email; -using Shesha.OmoNotifications.Configuration.Sms; -using Shesha.OmoNotifications.Emails.Gateways; -using Shesha.OmoNotifications.Helpers; -using Shesha.OmoNotifications.Sms.Gateways; +using Shesha.Notifications.Configuration; +using Shesha.Notifications.Configuration.Email; +using Shesha.Notifications.Configuration.Sms; +using Shesha.Notifications.Emails.Gateways; +using Shesha.Notifications.Helpers; +using Shesha.Notifications.Sms.Gateways; using System; using System.Collections.Generic; using System.Linq; @@ -21,7 +20,7 @@ using System.Threading.Tasks; using System.Web; -namespace Shesha.OmoNotifications.SMS +namespace Shesha.Notifications.SMS { public class SmsChannelSender : INotificationChannelSender { @@ -57,7 +56,7 @@ private async Task GetSettings() return await _notificationSettings.SmsSettings.GetValueAsync(); } - public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) { var settings = await GetSettings(); diff --git a/shesha-core/src/Shesha.Application/Notifications/SmsRealTimeNotifier.cs b/shesha-core/src/Shesha.Application/Notifications/SmsRealTimeNotifier.cs deleted file mode 100644 index 891aa6c518..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/SmsRealTimeNotifier.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Abp.Dependency; -using Abp.Domain.Repositories; -using Abp.Domain.Uow; -using Abp.Notifications; -using Hangfire; -using Shesha.Authorization.Users; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.Extensions; -using Shesha.NHibernate; -using Shesha.NotificationMessages.Dto; -using Shesha.Sms; -using Shesha.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Shesha.Notifications -{ - /// - /// SMS notifier - /// - public class SmsRealTimeNotifier : RealTimeNotifierBase, IShaRealTimeNotifier, ITransientDependency - { - private readonly ISmsGateway _smsGateway; - - public SmsRealTimeNotifier(UserManager userManager, IRepository personRepository, IRepository templateRepository, IRepository notificationMessageRepository, IUnitOfWorkManager uowManager, IRepository notificationRepository, ISmsGateway smsGateway) : base(userManager, personRepository, templateRepository, notificationMessageRepository, uowManager, notificationRepository) - { - _smsGateway = smsGateway; - } - - /// inheritedDoc - public override RefListNotificationType NotificationType => RefListNotificationType.SMS; - - public bool UseOnlyIfRequestedAsTarget => true; - - /// inheritedDoc - public async Task SendNotificationsAsync(UserNotification[] userNotifications) - { - try - { - if (!userNotifications.Any()) - return; - - foreach (var userNotification in userNotifications) - { - var template = await GetTemplateAsync(userNotification.Notification.NotificationName, RefListNotificationType.SMS); - if (template == null || !template.IsEnabled) - continue; - - if (template.SendType != RefListNotificationType.SMS) - throw new Exception($"Wrong type of template. Expected `{RefListNotificationType.SMS}`, actual `{template.SendType}`"); - - var person = await GetRecipientAsync(userNotification); - var mobileNo = person?.GetMobileNumber(); - if (string.IsNullOrWhiteSpace(mobileNo)) - continue; - - var body = await GenerateContentAsync(template.Body, userNotification); - - if (string.IsNullOrWhiteSpace(body)) - continue; - - // save to audit - var messageId = await CreateNotificationMessageAsync(template.Id, person.Id, message => - { - message.Body = body; - message.RecipientText = mobileNo; - //message.SourceEntity = userNotification.s; - }); - - // schedule sending - UowManager.Current.DoAfterTransaction(() => BackgroundJob.Enqueue(() => SendNotification(messageId))); - } - } - catch (Exception) - { - throw; - } - } - - /// inheritedDoc - protected override async Task DoSendNotificationMessageAsync(NotificationMessage message) - { - await _smsGateway.SendSmsAsync(message.RecipientText, message.Body); - } - - protected override void DoSendNotificationMessage(NotificationMessage message) - { - AsyncHelper.RunSync(() => _smsGateway.SendSmsAsync(message.RecipientText, message.Body)); - } - - /// inheritedDoc - public async Task SendNotificationsAsync(List notificationMessages) - { - await SendNotificationsAsync(notificationMessages, RefListNotificationType.SMS); - } - - public Task ResendMessageAsync(Guid notificationMessageId) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs similarity index 80% rename from shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs rename to shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs index 1f90b0d9da..690ea9ec25 100644 --- a/shesha-core/src/Shesha.Application/OmoNotifications/Teams/TeamsChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs @@ -1,7 +1,6 @@ using Shesha.Domain; using Shesha.Domain.Enums; using Shesha.Email.Dtos; -using Shesha.Notifications.Dto; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Shesha.OmoNotifications.Teams +namespace Shesha.Notifications.Teams { public class TeamsChannelSender : INotificationChannelSender { @@ -19,7 +18,7 @@ public string GetRecipientId(Person person) return person.EmailAddress1; } - public async Task SendAsync(Person fromPerson, Person toPerson, OmoNotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) + public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) { // Call Teams API to send message return await Task.FromResult(true); // Replace with actual API call diff --git a/shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs b/shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs deleted file mode 100644 index fcd8bb307b..0000000000 --- a/shesha-core/src/Shesha.Application/OmoNotifications/OmoNotificationAppService.cs +++ /dev/null @@ -1,351 +0,0 @@ -using Abp.Application.Services.Dto; -using Abp.BackgroundJobs; -using Abp.Domain.Entities; -using Abp.Domain.Repositories; -using Abp.Notifications; -using Abp.UI; -using DocumentFormat.OpenXml.Wordprocessing; -using Hangfire; -using Hangfire.Storage; -using Newtonsoft.Json.Linq; -using NHibernate.Linq; -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.DynamicEntities.Dtos; -using Shesha.EntityReferences; -using Shesha.NotificationMessages.Dto; -using Shesha.Notifications.Dto; -using Shesha.OmoNotifications.Configuration; -using Shesha.OmoNotifications.Dto; -using Shesha.OmoNotifications.Helpers; -using Shesha.OmoNotifications.Jobs; -using Shesha.Services; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Shesha.OmoNotifications -{ - public class OmoNotificationAppService: SheshaAppServiceBase - { - private readonly IEnumerable _channelSenders; - private readonly IRepository _notificationTypeRepository; - private readonly IRepository _notificationChannelRepository; - private readonly IRepository _messageTemplateRepository; - private readonly IRepository _notificationMessageRepository; - private readonly IRepository _userNotificationPreference; - private readonly IRepository _userTopicSubscriptionRepository; - private readonly IRepository _notificationTopicRepository; - private readonly IRepository _storedFileRepository; - private readonly IRepository _personRepository; - private readonly INotificationSettings _notificationSettings; - private readonly IStoredFileService _storedFileService; - private readonly IBackgroundJobManager _backgroundJobManager; - - public OmoNotificationAppService(IEnumerable channelSenders, - IRepository notificationTypeRepository, - IRepository notificationChannelRepository, - IRepository userNotificationPreference, - IRepository messageTemplateRepository, - IRepository personRepository, - IRepository storedFileRepository, - IRepository userTopicSubscriptionRepository, - IStoredFileService storedFileService, - IRepository notificationMessageRepository, - IRepository notificationTopicRepository, - INotificationSettings notificationSettings, - IBackgroundJobManager backgroundJobManager) - - { - _channelSenders = channelSenders; - _notificationTypeRepository = notificationTypeRepository; - _notificationChannelRepository = notificationChannelRepository; - _userNotificationPreference = userNotificationPreference; - _messageTemplateRepository = messageTemplateRepository; - _notificationSettings = notificationSettings; - _personRepository = personRepository; - _storedFileService = storedFileService; - _notificationMessageRepository = notificationMessageRepository; - _userTopicSubscriptionRepository = userTopicSubscriptionRepository; - _backgroundJobManager = backgroundJobManager; - _storedFileRepository = storedFileRepository; - _notificationTopicRepository = notificationTopicRepository; - } - - public class TestData: NotificationData - { - public string subject { get; set; } - public string name { get; set; } - public string body { get; set; } - } - - public async Task>> OmoTest() - { - var type = await _notificationTypeRepository.FirstOrDefaultAsync(x => x.Name == "Warning"); - var fromPerson = await _personRepository.FirstOrDefaultAsync(x => x.FirstName == "System"); - var toPerson = await _personRepository.FirstOrDefaultAsync(x => x.EmailAddress1 == "omolemo.lethuloe@boxfusion.io"); - var channel = await _notificationChannelRepository.FirstOrDefaultAsync(x => x.Name == "Email"); - var getAttachments = await _storedFileService.GetAttachmentsAsync(fromPerson.Id, "Shesha.Domain.Person"); - - var attachments = getAttachments.Select(x => new NotificationAttachmentDto() - { - FileName = x.FileName, - StoredFileId = x.Id, - }).ToList(); - - - var testing = new TestData() - { - name = "Omolemo", - subject = "Test Subject", - body = "Test Body" - }; - var triggeringEntity = new GenericEntityReference(fromPerson); - return await SendNotification(type, fromPerson, toPerson, data: testing, RefListNotificationPriority.High, attachments, triggeringEntity, channel); - } - - public async Task>> OmoBroadcastTest() - { - var type = await _notificationTypeRepository.FirstOrDefaultAsync(x => x.Name == "Warning"); - var topic = await _notificationTopicRepository.FirstOrDefaultAsync(x => x.Name == "Service Requests"); - var getAttachments = await _storedFileService.GetAttachmentsAsync(topic.Id, "Shesha.Core.NotificationTopic"); - - var attachments = getAttachments.Select(x => new NotificationAttachmentDto() - { - FileName = x.FileName, - StoredFileId = x.Id, - }).ToList(); - - - var testing = new TestData() - { - name = "Omolemo", - subject = "Test Subject", - body = "Test Body" - }; - var triggeringEntity = new GenericEntityReference(topic); - return await SendBroadcastNotification(type, topic, data: testing, RefListNotificationPriority.High, attachments, triggeringEntity); - } - - public async Task>> SendBroadcastNotification(NotificationTypeConfig type, NotificationTopic topic, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData: NotificationData - { - var notification = await SaveOrUpdateEntityAsync(null, item => - { - item.NotificationType = type; - item.NotificationTopic = topic; - item.NotificationData = data.ToString(); - item.Priority = (RefListNotificationPriority)priority; - item.TriggeringEntity = triggeringEntity; - }); - - await CurrentUnitOfWork.SaveChangesAsync(); - - - var users = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == topic.Id); - - if (channel != null) - { - var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channel.SupportedFormat == x.MessageFormat); - var subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data); - var message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); - - await _backgroundJobManager.EnqueueAsync(new BroadcastNotificationJobArgs() - { - TemplateId = template.Id, - NotificationId = notification.Id, - ChannelId = channel.Id, - Subject = subject, - Message = message, - Attachments = attachments - }); - } - else - { - var subscriptions = await _userTopicSubscriptionRepository.GetAllListAsync(x => x.Topic.Id == topic.Id); - - if (subscriptions != null && subscriptions.Any()) - { - foreach (var user in users) - { - var userChannels = await GetChannelsAsync(type, user.User, priority); - - foreach (var channelConfig in userChannels) - { - var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); - var subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate, data); - var message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); - - await _backgroundJobManager.EnqueueAsync(new BroadcastNotificationJobArgs() - { - TemplateId = template.Id, - NotificationId = notification.Id, - ChannelId = channelConfig.Id, - Subject = subject, - Message = message, - Attachments = attachments - }); - } - } - - } - } - - return await MapToDynamicDtoListAsync(new List()); - } - - - public async Task>> SendNotification(NotificationTypeConfig type, Person fromPerson, Person toPerson, TData data, RefListNotificationPriority priority, List attachments = null, GenericEntityReference triggeringEntity = null, NotificationChannelConfig channel = null) where TData: NotificationData - { - var notification = await SaveOrUpdateEntityAsync(null, item => - { - item.NotificationType = type; - item.FromPerson = fromPerson; - item.ToPerson = toPerson; - item.NotificationData = data.ToString(); - item.TriggeringEntity = triggeringEntity; - item.Priority = (RefListNotificationPriority)priority; - }); - - await CurrentUnitOfWork.SaveChangesAsync(); - - if (channel != null) - { - // Send notification to a specific channel - await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority,channel, attachments); - } - else - { - // Send notification to all determined channels - var channels = await GetChannelsAsync(type, toPerson, (RefListNotificationPriority)priority); - - foreach (var channelConfig in channels) - { - await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority, channelConfig, attachments); - } - } - - // Return the list of channels used for sending notifications as DynamicDto - return await MapToDynamicDtoListAsync(new List()); - } - - private async Task> GetChannelsAsync(NotificationTypeConfig type, Person recipient, RefListNotificationPriority priority) - { - List results = new List(); - - // Step 1: Check User Notification Preferences - var userPreferences = await _userNotificationPreference.GetAllListAsync( - x => x.User.Id == recipient.Id && x.NotificationType.Id == type.Id - ); - - if (userPreferences != null && userPreferences.Any()) - { - // Flatten and return DefaultChannel from user preferences if available - return userPreferences.Select(x => x.DefaultChannel).ToList(); - } - - // Step 2: Check Override Channels in NotificationTypeConfig - if (!string.IsNullOrEmpty(type.OverrideChannels)) - { - try - { - var overrideChannelIds = JsonSerializer.Deserialize(type.OverrideChannels); - - // Fetch channels by IDs if the repository supports it - var channels = await _notificationChannelRepository - .GetAll() - .Where(channel => overrideChannelIds.Contains(channel.Id)) - .ToListAsync(); - - if (channels.Any()) - { - return channels; - } - } - catch (JsonException ex) - { - new UserFriendlyException("Error deserializing override channels", ex); - // Log deserialization error (ex.Message) and continue to fallback - // Optionally handle the error depending on requirements - } - return results; - } - - // Step 3: Fallback - Return default channels based on priority (if applicable) - var notificationSettings = await _notificationSettings.NotificationSettings.GetValueAsync(); - switch (priority) - { - case RefListNotificationPriority.Low: - return notificationSettings.Low; - case RefListNotificationPriority.Medium: - return notificationSettings.Medium; - case RefListNotificationPriority.High: - return notificationSettings.High; - default: - return new List(); - }; - } - - private async Task ProcessAndSendNotificationToChannel(OmoNotification notification, TData data, Person fromPerson, Person toPerson, NotificationTypeConfig type, RefListNotificationPriority priority, NotificationChannelConfig channelConfig, List attachments = null) where TData: NotificationData - { - var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); - var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == channelConfig.SenderTypeName); - - if (senderChannelInterface == null) - throw new UserFriendlyException($"Sender not found for channel {channelConfig.Name}"); - - // Create a new notification message - var message = await SaveOrUpdateEntityAsync(null, item => - { - item.PartOf = notification; - item.Channel = channelConfig; - item.Subject = TemplateHelper.ReplacePlaceholders(template.TitleTemplate,data); - item.Message = TemplateHelper.ReplacePlaceholders(template.BodyTemplate, data); - item.RetryCount = 0; - item.ReadStatus = RefListNotificationReadStatus.Unread; - item.Direction = RefListNotificationDirection.Outgoing; - item.Status = RefListNotificationStatus.Preparing; - }); - await CurrentUnitOfWork.SaveChangesAsync(); - - // save attachments if specified - if (attachments != null) - { - foreach (var attachmentDto in attachments) - { - var file = await _storedFileRepository.GetAsync(attachmentDto.StoredFileId); - - await SaveOrUpdateEntityAsync(null, item => - { - item.PartOf = message; - item.File = file; - item.FileName = attachmentDto.FileName; - }); - } - } - - await CurrentUnitOfWork.SaveChangesAsync(); - - var sender = new NotificationSender(senderChannelInterface); - - if (type.IsTimeSensitive) - { - await sender.SendAsync(fromPerson, toPerson, message, true); - } - else - { - await _backgroundJobManager.EnqueueAsync(new DirectNotificationJobArgs() - { - SenderTypeName = channelConfig.SenderTypeName, - FromPerson = fromPerson.Id, - ToPerson = toPerson.Id, - Message = message.Id - }); - } - - await CurrentUnitOfWork.SaveChangesAsync(); - } - } -} diff --git a/shesha-core/src/Shesha.Application/Shesha.Application.csproj b/shesha-core/src/Shesha.Application/Shesha.Application.csproj index 78ca36042b..0bc6ebe22b 100644 --- a/shesha-core/src/Shesha.Application/Shesha.Application.csproj +++ b/shesha-core/src/Shesha.Application/Shesha.Application.csproj @@ -64,6 +64,10 @@ + + + + @@ -94,6 +98,10 @@ + + + + diff --git a/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs b/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs index f50e298bb5..90c914eec6 100644 --- a/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs +++ b/shesha-core/src/Shesha.Application/SheshaApplicationModule.cs @@ -1,4 +1,5 @@ using Abp; +using Abp.AspNetCore; using Abp.AspNetCore.Configuration; using Abp.AutoMapper; using Abp.Configuration.Startup; @@ -11,17 +12,20 @@ using Abp.Reflection.Extensions; using Castle.MicroKernel.Registration; using Shesha.Authorization; +using Shesha.ConfigurationItems.Distribution; using Shesha.Domain; using Shesha.DynamicEntities; using Shesha.Email; using Shesha.GraphQL; using Shesha.Modules; using Shesha.Notifications; -using Shesha.OmoNotifications; -using Shesha.OmoNotifications.Configuration; -using Shesha.OmoNotifications.Configuration.Email; -using Shesha.OmoNotifications.Configuration.Email.Gateways; -using Shesha.OmoNotifications.Configuration.Sms; +using Shesha.Notifications.Configuration; +using Shesha.Notifications.Configuration.Email; +using Shesha.Notifications.Configuration.Email.Gateways; +using Shesha.Notifications.Configuration.Sms; +using Shesha.Notifications.Distribution.NotificationChannels; +using Shesha.Notifications.Distribution.NotificationGateways; +using Shesha.Notifications.Distribution.NotificationTypes; using Shesha.Otp; using Shesha.Otp.Configuration; using Shesha.Reflection; @@ -40,7 +44,8 @@ namespace Shesha typeof(AbpKernelModule), typeof(SheshaCoreModule), typeof(SheshaGraphQLModule), - typeof(AbpAutoMapperModule))] + typeof(AbpAutoMapperModule), + typeof(AbpAspNetCoreModule))] public class SheshaApplicationModule : SheshaSubModule { public const int DefaultSingleMessageMaxLength = 160; @@ -58,13 +63,10 @@ public override void PreInitialize() IocManager.Register(); IocManager.Register(); - Configuration.Notifications.Providers.Add(); - Configuration.Notifications.Notifiers.Add(); - Configuration.Notifications.Notifiers.Add(); - Configuration.Authorization.Providers.Add(); Configuration.Authorization.Providers.Add(); + // replace email sender Configuration.ReplaceService(DependencyLifeStyle.Transient); @@ -72,8 +74,6 @@ public override void PreInitialize() //Configuration.Notifications.Distributers.Clear(); //Configuration.Notifications.Distributers.Add(); - Configuration.ReplaceService(DependencyLifeStyle.Transient); - IocManager.IocContainer.Register( Component.For().Forward().Forward().ImplementedBy().LifestyleTransient(), Component.For(typeof(IEntityReorderer<,,>)).ImplementedBy(typeof(EntityReorderer<,,>)).LifestyleTransient() @@ -88,12 +88,12 @@ public override void PreInitialize() Medium = new List { }, High = new List { }, }); - s.SmsSettings.WithDefaultValue(new Shesha.OmoNotifications.Configuration.Sms.SmsSettings + s.SmsSettings.WithDefaultValue(new Shesha.Notifications.Configuration.Sms.SmsSettings { SmsEnabled = true, PreferredGateway = null }); - s.EmailSettings.WithDefaultValue(new Shesha.OmoNotifications.Configuration.Email.EmailSettings + s.EmailSettings.WithDefaultValue(new Shesha.Notifications.Configuration.Email.EmailSettings { EmailsEnabled = true, PreferredGateway = null @@ -110,7 +110,7 @@ public override void PreInitialize() }); IocManager.RegisterSettingAccessor(s => { - s.ClickatellSettings.WithDefaultValue(new OmoNotifications.Configuration.Sms.Gateways.ClickatellSettings + s.ClickatellSettings.WithDefaultValue(new Notifications.Configuration.Sms.Gateways.ClickatellSettings { Host = "api.clickatell.com", SingleMessageMaxLength = DefaultSingleMessageMaxLength, @@ -181,6 +181,50 @@ public override void Initialize() var thisAssembly = Assembly.GetExecutingAssembly(); IocManager.RegisterAssemblyByConvention(thisAssembly); + IocManager.IocContainer.Register(Component + .For() + .Named("NotificationChannelExport") + .Forward>() + .Forward() + .ImplementedBy() + .LifestyleTransient()) + .Register(Component + .For() + .Named("NotificationChannelImport") + .Forward>() + .Forward() + .ImplementedBy() + .LifestyleTransient()) + .Register(Component + .For() + .Named("NotificationTypeExport") + .Forward>() + .Forward() + .ImplementedBy() + .LifestyleTransient()) + .Register(Component + .For() + .Named("NotificationTypeImport") + .Forward>() + .Forward() + .ImplementedBy() + .LifestyleTransient()) + .Register(Component + .For() + .Named("NotificationGatewayExport") + .Forward>() + .Forward() + .ImplementedBy() + .LifestyleTransient()) + .Register(Component + .For() + .Named("NotificationGatewayImport") + .Forward>() + .Forward() + .ImplementedBy() + .LifestyleTransient()); + + /* api not used now, this registration causes problems in the IoC. Need to solve IoC problem before uncommenting var schemaContainer = IocManager.Resolve(); var serviceProvider = IocManager.Resolve(); diff --git a/shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs b/shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs deleted file mode 100644 index f34e8b54b9..0000000000 --- a/shesha-core/src/Shesha.Core/Domain/MessageTemplate.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Abp.Domain.Entities.Auditing; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Domain -{ - [Entity(TypeShortAlias = "Shesha.Core.MessageTemplate")] - public class MessageTemplate: FullAuditedEntity - { - [ReferenceList("Shesha.Core", "NotificationMessageFormat")] - public virtual RefListNotificationMessageFormat? MessageFormat { get; set; } - - public virtual NotificationTypeConfig PartOf { get; set; } - [StringLength(2000)] - public virtual string TitleTemplate { get; set; } - [StringLength(int.MaxValue)] - public virtual string BodyTemplate { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Core/Domain/Notification.cs b/shesha-core/src/Shesha.Core/Domain/Notification.cs index 15d3b2c55e..0c5f3331e0 100644 --- a/shesha-core/src/Shesha.Core/Domain/Notification.cs +++ b/shesha-core/src/Shesha.Core/Domain/Notification.cs @@ -1,54 +1,52 @@ -using Abp.Domain.Entities.Auditing; -using Abp.Domain.Repositories; -using FluentValidation; -using Shesha.Domain.Attributes; -using Shesha.Extensions; -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using Shesha.EntityReferences; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Threading; +using System.Text; using System.Threading.Tasks; -namespace Shesha.Domain -{ - [Entity(FriendlyName = "Notification")] - public class Notification : FullAuditedEntity - { - [NotMapped] - public virtual string FullName => !string.IsNullOrEmpty(Namespace) ? Namespace + ": " + Name : !string.IsNullOrEmpty(Name) ? Name : Id.ToString(); - - [Required] - [StringLength(255)] - [EntityDisplayName] - public virtual string Name { get; set; } - - [StringLength(255)] - public virtual string Namespace { get; set; } - - [DataType(DataType.MultilineText)] - [StringLength(int.MaxValue)] - public virtual string Description { get; set; } - } - - public class NotificationValidator : AbstractValidator - { - private readonly IRepository _repository; - - public NotificationValidator(IRepository repository) - { - _repository = repository; - RuleFor(x => x.Name).NotEmpty().MustAsync(UniqueNameAsync).WithMessage("Notification with name '{PropertyValue}' already exists."); - } - - private async Task UniqueNameAsync(Notification notification, string name, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name)) - return true; - - var alreadyExist = await _repository.GetAll().Where(m => m.Name.ToLower() == name.ToLower() && m.Id != notification.Id).AnyAsync(); - return !alreadyExist; - } - } - -} +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.Notification")] + public class Notification: FullAuditedEntity + { + /// + /// + /// + public virtual string Name { get; set; } + /// + /// + /// + public virtual NotificationTypeConfig NotificationType { get; set; } + /// + /// + /// + public virtual Person ToPerson { get; set; } + /// + /// + /// + public virtual Person FromPerson { get; set; } + /// + /// Serialized Json of the notification data + /// + [StringLength(int.MaxValue)] + public virtual string NotificationData { get; set; } + /// + /// + /// + [ReferenceList("Shesha.Core", "NotificationPriority")] + public virtual RefListNotificationPriority Priority { get; set; } + /// + /// The entity that the notification pertains to + /// + public virtual GenericEntityReference TriggeringEntity { get; set; } + /// + /// + /// + public virtual NotificationTopic NotificationTopic { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs b/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs index f31ad0d78e..e6f3d74e2b 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationMessage.cs @@ -1,113 +1,71 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Abp.Domain.Entities; -using Abp.Domain.Entities.Auditing; -using Abp.Notifications; +using Abp.Domain.Entities; +using Shesha.Domain.Attributes; using Shesha.Domain.Enums; -using Shesha.EntityReferences; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace Shesha.Domain { + [Entity(TypeShortAlias = "Shesha.Core.NotificationMessage")] public class NotificationMessage : FullAuditedEntityWithExternalSync, IMayHaveTenant { /// - /// Person who sent this message + /// /// - public virtual Person Sender { get; set; } - + public virtual Notification PartOf { get; set; } /// - /// Recipient (person) of the message + /// /// - public virtual Person Recipient { get; set; } - - /// - /// Send type (email/sms/push etc) - /// - public virtual RefListNotificationType SendType { get; set; } - + public virtual NotificationChannelConfig Channel { get; set; } /// - /// Recipient text (email address/mobile number etc) + /// /// - [StringLength(300)] public virtual string RecipientText { get; set; } /// - /// Sender text (email address/mobile number etc) + /// /// - [StringLength(300)] - public virtual string SenderText { get; set; } - - /// - /// Subject of the message - /// - [StringLength(300)] public virtual string Subject { get; set; } - /// - /// Message body + /// /// - [StringLength(int.MaxValue)] - public virtual string Body { get; set; } - - /// - /// Template that was used for message generation - /// - public virtual NotificationTemplate Template { get; set; } - + public virtual string Message { get; set; } /// - /// Notification - /// - public virtual Notification Notification { get; set; } - - /// - /// Source or Owner Entity + /// Number of attempt to send the message /// - public virtual GenericEntityReference SourceEntity { get; set; } - + public virtual int RetryCount { get; set; } /// - /// Date and time of last attempt to send the message + /// Direction (outgoing/incoming) /// - public virtual DateTime? SendDate { get; set; } - + [ReferenceList("Shesha.Core", "NotificationDirection")] + public virtual RefListNotificationDirection? Direction { get; set; } /// - /// Number of attempt to send the message + /// /// - public virtual int TryCount { get; set; } - + [ReferenceList("Shesha.Core", "NotificationReadStatus")] + public virtual RefListNotificationReadStatus? ReadStatus { get; set; } /// - /// Status (outgoing/sent/failed etc) + /// /// - public virtual RefListNotificationStatus Status { get; set; } - + public virtual DateTime? FirstDateRead { get; set; } /// - /// Direction (outgoing/incoming) + /// /// - public virtual RefListNotificationDirection Direction { get; set; } - + public virtual DateTime? DateSent { get; set; } /// - /// Error message + /// /// public virtual string ErrorMessage { get; set; } - /// - /// CC emails + /// /// - public virtual string Cc { get; set; } - - public NotificationMessage() - { - TryCount = 0; - Status = RefListNotificationStatus.Unknown; - } - public virtual int? TenantId { get; set; } - public virtual TenantNotificationInfo TenantNotification { get; set; } /// - /// Indicates whether or not a user has viewed the notificationMessage, - /// - public virtual bool? Opened { get; set; } - /// - /// The Date and time the user last viewed the notificationMessage + /// Status (outgoing/sent/failed etc) /// - public virtual DateTime? LastOpened { get; set; } + [ReferenceList("Shesha.Core", "NotificationStatus")] + public virtual RefListNotificationStatus Status { get; set; } } } diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs b/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs index 7a6a0b59ee..419a32ba3c 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationTemplate.cs @@ -1,62 +1,25 @@ -using Abp.Domain.Entities.Auditing; -using Abp.Domain.Repositories; -using FluentValidation; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using Shesha.Extensions; -using System; -using System.ComponentModel.DataAnnotations; +using Abp.Domain.Entities.Auditing; +using Shesha.Domain.Attributes; +using Shesha.Domain.Enums; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Threading; +using System.Text; using System.Threading.Tasks; -namespace Shesha.Domain -{ - [Entity(TypeShortAlias = "Shesha.Core.NotificationTemplate")] - public class NotificationTemplate : FullAuditedEntity - { - public virtual bool IsEnabled { get; set; } - - [StringLength(300)] - public virtual string Name { get; set; } - - [Required] - - public virtual Notification Notification { get; set; } - - [StringLength(int.MaxValue)] - public virtual string Body { get; set; } - - [StringLength(300)] - public virtual string Subject { get; set; } - public virtual RefListNotificationType? SendType { get; set; } - public virtual RefListNotificationTemplateType? BodyFormat { get; set; } - - public NotificationTemplate() - { - IsEnabled = true; - } - } - - public class NotificationTemplateValidator : AbstractValidator +namespace Shesha.Domain +{ + [Entity(TypeShortAlias = "Shesha.Core.NotificationTemplate")] + public class NotificationTemplate: FullAuditedEntity { - private readonly IRepository _repository; - - public NotificationTemplateValidator(IRepository repository) - { - _repository = repository; - - RuleFor(x => x.Notification).NotEmpty(); - RuleFor(x => x.Name).NotEmpty().MustAsync(UniqueNameAsync).WithMessage("Template with name '{PropertyValue}' already exists in this notification."); - } - - private async Task UniqueNameAsync(NotificationTemplate template, string name, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name) || template.Notification == null) - return true; - - var alreadyExist = await _repository.GetAll().Where(m => m.Notification == template.Notification && m.Name.ToLower() == name.ToLower() && m.Id != template.Id).AnyAsync(); - return !alreadyExist; - } - } -} + [ReferenceList("Shesha.Core", "NotificationMessageFormat")] + public virtual RefListNotificationMessageFormat? MessageFormat { get; set; } + + public virtual NotificationTypeConfig PartOf { get; set; } + [StringLength(2000)] + public virtual string TitleTemplate { get; set; } + [StringLength(int.MaxValue)] + public virtual string BodyTemplate { get; set; } + } +} diff --git a/shesha-core/src/Shesha.Core/Domain/OmoNotification.cs b/shesha-core/src/Shesha.Core/Domain/OmoNotification.cs deleted file mode 100644 index 753f2439dd..0000000000 --- a/shesha-core/src/Shesha.Core/Domain/OmoNotification.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Abp.Domain.Entities.Auditing; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using Shesha.EntityReferences; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Domain -{ - [Entity(TypeShortAlias = "Shesha.Core.OmoNotification")] - public class OmoNotification: FullAuditedEntity - { - /// - /// - /// - public virtual string Name { get; set; } - /// - /// - /// - public virtual NotificationTypeConfig NotificationType { get; set; } - /// - /// - /// - public virtual Person ToPerson { get; set; } - /// - /// - /// - public virtual Person FromPerson { get; set; } - /// - /// Serialized Json of the notification data - /// - [StringLength(int.MaxValue)] - public virtual string NotificationData { get; set; } - /// - /// - /// - [ReferenceList("Shesha.Core", "NotificationPriority")] - public virtual RefListNotificationPriority Priority { get; set; } - /// - /// The entity that the notification pertains to - /// - public virtual GenericEntityReference TriggeringEntity { get; set; } - /// - /// - /// - public virtual NotificationTopic NotificationTopic { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs b/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs deleted file mode 100644 index a307bc6d1b..0000000000 --- a/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessage.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Abp.Domain.Entities; -using Shesha.Domain.Attributes; -using Shesha.Domain.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Domain -{ - [Entity(TypeShortAlias = "Shesha.Core.OmoNotificationMessage")] - public class OmoNotificationMessage : FullAuditedEntityWithExternalSync, IMayHaveTenant - { - /// - /// - /// - public virtual OmoNotification PartOf { get; set; } - /// - /// - /// - public virtual NotificationChannelConfig Channel { get; set; } - /// - /// - /// - public virtual string RecipientText { get; set; } - /// - /// - /// - public virtual string Subject { get; set; } - /// - /// - /// - public virtual string Message { get; set; } - /// - /// Number of attempt to send the message - /// - public virtual int RetryCount { get; set; } - /// - /// Direction (outgoing/incoming) - /// - [ReferenceList("Shesha.Core", "NotificationDirection")] - public virtual RefListNotificationDirection? Direction { get; set; } - /// - /// - /// - [ReferenceList("Shesha.Core", "NotificationReadStatus")] - public virtual RefListNotificationReadStatus? ReadStatus { get; set; } - /// - /// - /// - public virtual DateTime? FirstDateRead { get; set; } - /// - /// - /// - public virtual DateTime? DateSent { get; set; } - /// - /// - /// - public virtual string ErrorMessage { get; set; } - /// - /// - /// - public virtual int? TenantId { get; set; } - /// - /// Status (outgoing/sent/failed etc) - /// - [ReferenceList("Shesha.Core", "NotificationStatus")] - public virtual RefListNotificationStatus Status { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs b/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs deleted file mode 100644 index 1ba80b5a62..0000000000 --- a/shesha-core/src/Shesha.Core/Domain/OmoNotificationMessageAttachment.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Abp.Domain.Entities.Auditing; - -namespace Shesha.Domain -{ - public class OmoNotificationMessageAttachment : FullAuditedEntity - { - /// - /// Name of the file, is used for overriding of the File's name - /// - [StringLength(300)] - public virtual string FileName { get; set; } - - /// - /// Stored file - /// - public virtual StoredFile File { get; set; } - - /// - /// Stored file - /// - public virtual OmoNotificationMessage PartOf { get; set; } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs b/shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs deleted file mode 100644 index 38d8068c33..0000000000 --- a/shesha-core/src/Shesha.Core/Migrations/M20241111155000.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Migrations -{ - [Migration(20241111155000)] - public class M20241111155000 : Migration - { - public override void Up() - { - Alter.Table("Core_NotificationTypeConfigs") - .AddColumn("Core_IsTimeSensitive").AsBoolean().WithDefaultValue(false); - } - public override void Down() - { - throw new NotImplementedException(); - } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs b/shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs deleted file mode 100644 index 9b83a2e416..0000000000 --- a/shesha-core/src/Shesha.Core/Migrations/M20241111160500.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Migrations -{ - [Migration(20241111160500)] - public class M20241111160500 : Migration - { - public override void Up() - { - Alter.Table("Core_OmoNotificationMessages") - .AddColumn("StatusLkp").AsInt64().Nullable(); - } - public override void Down() - { - throw new NotImplementedException(); - } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs b/shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs deleted file mode 100644 index f79e4020fd..0000000000 --- a/shesha-core/src/Shesha.Core/Migrations/M20241114141100.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Migrations -{ - [Migration(20241114141100)] - public class M20241114141100 : Migration - { - public override void Up() - { - Create.Table("Core_OmoNotificationMessageAttachments") - .WithIdAsGuid() - .WithFullAuditColumns() - .WithColumn("FileName").AsString(300).Nullable() - .WithForeignKeyColumn("PartOfId", "Core_NotificationMessages") - .WithForeignKeyColumn("FileId", "Frwk_StoredFiles"); - } - public override void Down() - { - throw new NotImplementedException(); - } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs b/shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs deleted file mode 100644 index f70ae4418c..0000000000 --- a/shesha-core/src/Shesha.Core/Migrations/M20241114143900.cs +++ /dev/null @@ -1,31 +0,0 @@ -using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Migrations -{ - [Migration(20241114143900)] - public class M20241114143900 : Migration - { - public override void Up() - { - Delete.ForeignKey("FK_Core_OmoNotificationMessageAttachments_PartOfId_Core_NotificationMessages_Id").OnTable("Core_OmoNotificationMessageAttachments"); - Delete.Index("IX_Core_OmoNotificationMessageAttachments_PartOfId").OnTable("Core_OmoNotificationMessageAttachments"); - Delete.Column("PartOfId").FromTable("Core_OmoNotificationMessageAttachments"); - - - Alter.Table("Core_OmoNotificationMessageAttachments") - .AddForeignKeyColumn("PartOfId", "Core_OmoNotificationMessages"); - } - public override void Down() - { - throw new NotImplementedException(); - } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs b/shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs deleted file mode 100644 index 9fc0bfb85c..0000000000 --- a/shesha-core/src/Shesha.Core/Migrations/M20241114153500.cs +++ /dev/null @@ -1,26 +0,0 @@ -using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Migrations -{ - [Migration(20241114153500)] - public class M20241114153500 : Migration - { - public override void Up() - { - Alter.Table("Core_OmoNotifications") - .AddForeignKeyColumn("NotificationTopicId", "Core_NotificationTopics"); - } - public override void Down() - { - throw new NotImplementedException(); - } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs b/shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs deleted file mode 100644 index 31afa03015..0000000000 --- a/shesha-core/src/Shesha.Core/Migrations/M20241121180900.cs +++ /dev/null @@ -1,34 +0,0 @@ -using FluentMigrator; -using NUglify; -using Shesha.FluentMigrator; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Migrations -{ - [Migration(20241121180900)] - public class M20241121180900 : Migration - { - public override void Up() - { - Create.Table("Core_NotificationGatewayConfigs") - .WithIdAsGuid() - .WithForeignKeyColumn("Core_PartOfId", "Core_NotificationChannelConfigs") - .WithColumn("Core_GatewayTypeName").AsString(255).Nullable(); - - Create.ForeignKey("FK_Core_NotificationGatewayConfigs_Frwk_ConfigurationItems_Id") - .FromTable("Core_NotificationGatewayConfigs") - .ForeignColumn("Id") - .ToTable("Frwk_ConfigurationItems") - .PrimaryColumn("Id"); - } - public override void Down() - { - throw new NotImplementedException(); - } - } -} diff --git a/shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs b/shesha-core/src/Shesha.Core/Migrations/M20241127170300.cs similarity index 52% rename from shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs rename to shesha-core/src/Shesha.Core/Migrations/M20241127170300.cs index 78cedd7a0a..d44e61fa48 100644 --- a/shesha-core/src/Shesha.Core/Migrations/M20241106152600.cs +++ b/shesha-core/src/Shesha.Core/Migrations/M20241127170300.cs @@ -10,11 +10,12 @@ namespace Shesha.Migrations { - [Migration(20241106152600)] - public class M20241106152600 : Migration + [Migration(20241127170300)] + public class M20241127170300 : Migration { public override void Up() { + // NotificationTypes Create.Table("Core_NotificationTypeConfigs") .WithIdAsGuid() .WithColumn("Core_AllowAttachments").AsBoolean().WithDefaultValue(false) @@ -22,6 +23,7 @@ public override void Up() .WithColumn("Core_CanOtpOut").AsBoolean().WithDefaultValue(false) .WithColumn("Core_Category").AsString(255).Nullable() .WithColumn("Core_OrderIndex").AsInt32().Nullable() + .WithColumn("Core_IsTimeSensitive").AsBoolean().WithDefaultValue(false) .WithColumn("Core_OverrideChannels").AsStringMax().Nullable(); Create.ForeignKey("FK_Core_NotificationTypeConfigs_Frwk_ConfigurationItems_Id") @@ -30,6 +32,7 @@ public override void Up() .ToTable("Frwk_ConfigurationItems") .PrimaryColumn("Id"); + // NotificationChannels Create.Table("Core_NotificationChannelConfigs") .WithIdAsGuid() .WithColumn("Core_SupportedFormatLkp").AsInt64().Nullable() @@ -45,9 +48,43 @@ public override void Up() .ToTable("Frwk_ConfigurationItems") .PrimaryColumn("Id"); + // NotificationGateways + Create.Table("Core_NotificationGatewayConfigs") + .WithIdAsGuid() + .WithForeignKeyColumn("Core_PartOfId", "Core_NotificationChannelConfigs") + .WithColumn("Core_GatewayTypeName").AsString(255).Nullable(); + + Create.ForeignKey("FK_Core_NotificationGatewayConfigs_Frwk_ConfigurationItems_Id") + .FromTable("Core_NotificationGatewayConfigs") + .ForeignColumn("Id") + .ToTable("Frwk_ConfigurationItems") + .PrimaryColumn("Id"); + + // User Notification Preferences + Create.Table("Core_UserNotificationPreferences") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithForeignKeyColumn("UserId", "Core_Persons") + .WithForeignKeyColumn("NotificationTypeId", "Core_NotificationTypeConfigs") + .WithColumn("OptOut").AsBoolean().WithDefaultValue(false) + .WithForeignKeyColumn("DefaultChannelId", "Core_NotificationChannelConfigs"); + + // Notification Topics + Create.Table("Core_NotificationTopics") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithColumn("Name").AsString(255).NotNullable() + .WithColumn("Description").AsString(int.MaxValue).Nullable(); + + // User Topic Subscriptions + Create.Table("Core_UserTopicSubscriptions") + .WithIdAsGuid() + .WithFullAuditColumns() + .WithForeignKeyColumn("TopicId", "Core_NotificationTopics") + .WithForeignKeyColumn("UserId", "Core_Persons"); - ///// TODO: Decide between adding new entity or altering the 'NotificationTemplate' entity - Create.Table("Core_MessageTemplates") + // New NotificationTemplates + Create.Table("Core_NewNotificationTemplates") .WithIdAsGuid() .WithFullAuditColumns() .WithForeignKeyColumn("PartOfId", "Core_NotificationTypeConfigs") @@ -55,24 +92,26 @@ public override void Up() .WithColumn("BodyTemplate").AsString(int.MaxValue).Nullable() .WithColumn("MessageFormatLkp").AsInt64().Nullable(); - - Create.Table("Core_OmoNotifications") + // New Notifications + Create.Table("Core_NewNotifications") .WithIdAsGuid() .WithFullAuditColumns() .WithColumn("Name").AsString().Nullable() .WithForeignKeyColumn("NotificationTypeId", "Core_NotificationTypeConfigs") .WithForeignKeyColumn("ToPersonId", "Core_Persons") .WithForeignKeyColumn("FromPersonId", "Core_Persons") + .WithForeignKeyColumn("NotificationTopicId", "Core_NotificationTopics") .WithColumn("NotificationData").AsString(int.MaxValue).Nullable() .WithColumn("PriorityLkp").AsInt64().Nullable() .WithColumn("TriggeringEntityId").AsString(100).Nullable() .WithColumn("TriggeringEntityClassName").AsString(1000).Nullable() .WithColumn("TriggeringEntityDisplayName").AsString(1000).Nullable(); - Create.Table("Core_OmoNotificationMessages") + // New NotificationMessages + Create.Table("Core_NewNotificationMessages") .WithIdAsGuid() .WithFullAuditColumns() - .WithForeignKeyColumn("PartOfId", "Core_OmoNotifications") + .WithForeignKeyColumn("PartOfId", "Core_NewNotifications") .WithForeignKeyColumn("ChannelId", "Core_NotificationChannelConfigs") .WithColumn("RecipientText").AsString(int.MaxValue).Nullable() .WithColumn("Subject").AsString(1000).Nullable() @@ -83,6 +122,7 @@ public override void Up() .WithColumn("FirstDateRead").AsDateTime().Nullable() .WithColumn("DateSent").AsDateTime().Nullable() .WithColumn("ErrorMessage").AsString(int.MaxValue).Nullable() + .WithColumn("StatusLkp").AsInt64().Nullable() .WithColumn(DatabaseConsts.ExtSysFirstSyncDate).AsDateTime().Nullable() .WithColumn(DatabaseConsts.ExtSysId).AsString(50).Nullable() .WithColumn(DatabaseConsts.ExtSysLastSyncDate).AsDateTime().Nullable() @@ -90,28 +130,72 @@ public override void Up() .WithColumn(DatabaseConsts.ExtSysSyncError).AsStringMax().Nullable() .WithColumn(DatabaseConsts.ExtSysSyncStatusLkp).AsInt32().Nullable(); - Alter.Table("Core_OmoNotificationMessages") + Alter.Table("Core_NewNotificationMessages") .AddTenantIdColumnAsNullable(); - Create.Table("Core_UserNotificationPreferences") - .WithIdAsGuid() - .WithFullAuditColumns() - .WithForeignKeyColumn("UserId", "Core_Persons") - .WithForeignKeyColumn("NotificationTypeId", "Core_NotificationTypeConfigs") - .WithColumn("OptOut").AsBoolean().WithDefaultValue(false) - .WithForeignKeyColumn("DefaultChannelId", "Core_NotificationChannelConfigs"); + Execute.Sql(@" + INSERT INTO Core_NewNotifications (Id, CreationTime, Name, ToPersonId, FromPersonId, TriggeringEntityId, TriggeringEntityClassName, TriggeringEntityDisplayName) + SELECT + NEWID(), + nm.CreationTime, + n.Name, + nm.RecipientId, + nm.SenderId, + nm.SourceEntityId, + nm.SourceEntityClassName, + nm.SourceEntityDisplayName + FROM Core_Notifications n + LEFT JOIN Core_NotificationMessages nm ON n.Id = nm.NotificationId + WHERE nm.IsDeleted = 0 AND n.IsDeleted = 0; + "); - Create.Table("Core_NotificationTopics") - .WithIdAsGuid() - .WithFullAuditColumns() - .WithColumn("Name").AsString(255).NotNullable() - .WithColumn("Description").AsString(int.MaxValue).Nullable(); + Execute.Sql(@" + INSERT INTO Core_NewNotificationMessages (Id, CreationTime, PartOfId, ChannelId, RecipientText, Subject, Message,RetryCount, DirectionLkp, ReadStatusLkp, FirstDateRead,DateSent, ErrorMessage, StatusLkp) + SELECT + Id, + CreationTime, + NotificationId, + CASE + WHEN SendTypeLkp = 1 THEN + (SELECT Id FROM Core_NotificationChannelConfigs ncc WHERE ncc.Core_SenderTypeName = 'EmailChannelSender') + WHEN SendTypeLkp = 2 THEN + (SELECT Id FROM Core_NotificationChannelConfigs ncc WHERE ncc.Core_SenderTypeName = 'SmsChannelSender') + ELSE NULL + END, + RecipientText, + Subject, + Body, + TryCount, + DirectionLkp, + CASE WHEN Opened = 1 THEN 0 ELSE 1 END, + LastOpened, + SendDate, + ErrorMessage, + StatusLkp + FROM Core_NotificationMessages + WHERE IsDeleted = 0; + "); - Create.Table("Core_UserTopicSubscriptions") - .WithIdAsGuid() - .WithFullAuditColumns() - .WithForeignKeyColumn("TopicId", "Core_NotificationTopics") - .WithForeignKeyColumn("UserId", "Core_Persons"); + + Execute.Sql(@" + INSERT INTO Core_NewNotificationTemplates (Id, TitleTemplate, BodyTemplate, MessageFormatLkp) + SELECT + Id, + Subject, + Body, + BodyFormatLkp + FROM Core_NotificationTemplates + WHERE IsDeleted = 0; + "); + + + Rename.Table("Core_NotificationMessages").To("Core_OldNotificationMessages"); + Rename.Table("Core_NotificationTemplates").To("Core_OldNotificationTemplates"); + Rename.Table("Core_Notifications").To("Core_OldNotifications"); + + Rename.Table("Core_NewNotifications").To("Core_Notifications"); + Rename.Table("Core_NewNotificationMessages").To("Core_NotificationMessages"); + Rename.Table("Core_NewNotificationTemplates").To("Core_NotificationTemplates"); } public override void Down() { diff --git a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs index 8e1650f450..a20046b32c 100644 --- a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs +++ b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs @@ -31,13 +31,11 @@ using Shesha.GraphQL.Middleware; using Shesha.GraphQL.Swagger; using Shesha.Identity; -using Shesha.OmoNotifications; -using Shesha.OmoNotifications.Configuration.Sms.Gateways; -using Shesha.OmoNotifications.Emails; -using Shesha.OmoNotifications.Emails.Gateways; -using Shesha.OmoNotifications.Sms.Gateways; -using Shesha.OmoNotifications.SMS; -using Shesha.OmoNotifications.Teams; +using Shesha.Notifications; +using Shesha.Notifications.Emails.Gateways; +using Shesha.Notifications.Sms.Gateways; +using Shesha.Notifications.SMS; +using Shesha.Notifications.Teams; using Shesha.Scheduler.Extensions; using Shesha.Scheduler.Hangfire; using Shesha.Specifications; From dddc9822050a922a1d6d84539da43a62d6978f6a Mon Sep 17 00:00:00 2001 From: Omolemo Lethuloe Date: Wed, 27 Nov 2024 21:47:20 +0200 Subject: [PATCH 3/5] Removing teams sender --- .../Notifications/Teams/TeamsChannelSender.cs | 33 ------------------- .../src/Shesha.Web.Host/Startup/Startup.cs | 2 -- 2 files changed, 35 deletions(-) delete mode 100644 shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs diff --git a/shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs deleted file mode 100644 index 690ea9ec25..0000000000 --- a/shesha-core/src/Shesha.Application/Notifications/Teams/TeamsChannelSender.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Shesha.Domain; -using Shesha.Domain.Enums; -using Shesha.Email.Dtos; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Mail; -using System.Text; -using System.Threading.Tasks; - -namespace Shesha.Notifications.Teams -{ - public class TeamsChannelSender : INotificationChannelSender - { - public string GetRecipientId(Person person) - { - // Logic to get Teams ID or use email - return person.EmailAddress1; - } - - public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null) - { - // Call Teams API to send message - return await Task.FromResult(true); // Replace with actual API call - } - - public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) - { - return await Task.FromResult(false); - } - } - -} diff --git a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs index a20046b32c..66c8cfc992 100644 --- a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs +++ b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs @@ -35,7 +35,6 @@ using Shesha.Notifications.Emails.Gateways; using Shesha.Notifications.Sms.Gateways; using Shesha.Notifications.SMS; -using Shesha.Notifications.Teams; using Shesha.Scheduler.Extensions; using Shesha.Scheduler.Hangfire; using Shesha.Specifications; @@ -101,7 +100,6 @@ public IServiceProvider ConfigureServices(IServiceCollection services) IdentityRegistrar.Register(services); AuthConfigurer.Configure(services, _appConfiguration); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddTransient(); From 9c325018d45fa7e9104d6e039cf3694284ae59fb Mon Sep 17 00:00:00 2001 From: Omolemo Lethuloe Date: Thu, 28 Nov 2024 11:34:36 +0200 Subject: [PATCH 4/5] PR comments --- .../Notifications/Emails/EmailChannelSender.cs | 4 ++-- .../Notifications/Emails/Gateways/IEmailGateway.cs | 2 +- .../Notifications/Emails/Gateways/SmtpGateway.cs | 6 +++--- .../Notifications/INotificationChannelSender.cs | 4 ++-- .../Jobs/BroadcastNotificationJobQueuer.cs | 2 +- .../Jobs/DirectNotificationJobQueuer.cs | 3 ++- .../Notifications/NotificationAppService.cs | 8 ++++---- .../Notifications/NotificationSender.cs | 12 ++++++------ .../Notifications/Sms/SmsChannelSender.cs | 4 ++-- .../Shesha.Core/Domain/NotificationChannelConfig.cs | 2 +- .../Shesha.Core/Domain/NotificationGatewayConfig.cs | 2 +- .../src/Shesha.Core/Domain/NotificationTypeConfig.cs | 2 +- .../Configuration/SheshaSettingNames.cs | 2 -- 13 files changed, 26 insertions(+), 27 deletions(-) diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs index b7669ae1a1..1e1f16616a 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs @@ -86,14 +86,14 @@ public async Task SendAsync(Person fromPerson, Person toPerson, Notificati return await gateway.SendAsync(GetRecipientId(fromPerson), GetRecipientId(toPerson), message, isBodyHtml, cc, throwException, attachments); } - public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) + public async Task> BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) { var settings = await GetSettings(); if (!settings.EmailsEnabled) { Logger.Warn("Emails are disabled"); - return await Task.FromResult(false); + return await Task.FromResult(new Tuple(false, "Emails are disabled")); } var gateway = _emailGatewayFactory.GetGateway(settings.PreferredGateway.GatewayTypeName); diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs index ef36ef9bf9..316b20c5d0 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs @@ -11,7 +11,7 @@ namespace Shesha.Notifications.Emails.Gateways public interface IEmailGateway { Task SendAsync(string fromPerson, string toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null); - Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); + Task> BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); } } diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs index eed40d099e..16c115f35b 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs @@ -68,7 +68,7 @@ public Task SendAsync(string fromPerson, string toPerson, NotificationMess }; } - public Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null) + public Task> BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null) { using (var mail = BuildMessageWith(null, topicSubscribers, subject, message, true)) { @@ -82,13 +82,13 @@ public Task BroadcastAsync(string topicSubscribers, string subject, string try { SendEmail(mail); - return Task.FromResult(true); + return Task.FromResult(new Tuple(true, "Successfully Sent!")); } catch (Exception e) { // Log the exception Logger.Error("Failed to send email", e); - return Task.FromResult(false); + return Task.FromResult(new Tuple(true, e.Message)); } }; } diff --git a/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs index 386e519c21..8a5344f079 100644 --- a/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/INotificationChannelSender.cs @@ -15,6 +15,6 @@ public interface INotificationChannelSender { string GetRecipientId(Person person); Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml, string cc, bool throwException = false, List attachments = null); - Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null); + Task> BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null); } -} +} \ No newline at end of file diff --git a/shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs b/shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs index 8421889858..817463e137 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Jobs/BroadcastNotificationJobQueuer.cs @@ -82,7 +82,7 @@ public override async Task ExecuteAsync(BroadcastNotificationJobArgs args) var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == channel.SenderTypeName); if (senderChannelInterface == null) throw new Exception($"No sender found for sender type: {channel.SenderTypeName}"); - var sender = new NotificationSender(senderChannelInterface); + var sender = StaticContext.IocManager.Resolve(senderChannelInterface); await sender.SendBroadcastAsync(notification, message.Subject, message.Message, message.Attachments); } diff --git a/shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs b/shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs index c5ae75a1e8..c7b03afa0b 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Jobs/DirectNotificationJobQueuer.cs @@ -5,6 +5,7 @@ using Shesha.Domain; using Shesha.DynamicEntities.Dtos; using Shesha.Notifications.Dto; +using Shesha.Services; using System; using System.Collections.Generic; using System.Linq; @@ -37,7 +38,7 @@ public override async Task ExecuteAsync(DirectNotificationJobArgs args) var senderChannelInterface = _channelSenders.FirstOrDefault(x => x.GetType().Name == args.SenderTypeName); if (senderChannelInterface == null) throw new Exception($"No sender found for sender type: {args.SenderTypeName}"); - var sender = new NotificationSender(senderChannelInterface); + var sender = StaticContext.IocManager.Resolve(senderChannelInterface); await sender.SendAsync(fromPerson, toPerson, notificationMessage, true); } } diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs index 180c87bbfe..e363dfc072 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs @@ -179,7 +179,7 @@ public async Task>> SendNotific if (channel != null) { // Send notification to a specific channel - await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority,channel, attachments); + await SendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority,channel, attachments); } else { @@ -188,7 +188,7 @@ public async Task>> SendNotific foreach (var channelConfig in channels) { - await ProcessAndSendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority, channelConfig, attachments); + await SendNotificationToChannel(notification, data, fromPerson, toPerson, type, priority, channelConfig, attachments); } } @@ -274,7 +274,7 @@ private async Task> GetChannelsAsync(Notificatio /// /// /// - private async Task ProcessAndSendNotificationToChannel(Notification notification, TData data, Person fromPerson, Person toPerson, NotificationTypeConfig type, RefListNotificationPriority priority, NotificationChannelConfig channelConfig, List attachments = null) where TData: NotificationData + private async Task SendNotificationToChannel(Notification notification, TData data, Person fromPerson, Person toPerson, NotificationTypeConfig type, RefListNotificationPriority priority, NotificationChannelConfig channelConfig, List attachments = null) where TData: NotificationData { var template = await _messageTemplateRepository.FirstOrDefaultAsync(x => x.PartOf.Id == type.Id && channelConfig.SupportedFormat == x.MessageFormat); @@ -318,7 +318,7 @@ await SaveOrUpdateEntityAsync(null, item => await CurrentUnitOfWork.SaveChangesAsync(); - var sender = new NotificationSender(senderChannelInterface); + var sender = StaticContext.IocManager.Resolve(senderChannelInterface); if (type.IsTimeSensitive) { diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs index 26ce0b00eb..03912075ae 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs @@ -114,13 +114,13 @@ public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMess public async Task SendBroadcastAsync(Notification notification, string subject, string messageContent, List attachments) { int attempt = 0; - bool sentSuccessfully = false; + Tuple sentSuccessfully = new Tuple(false, ""); var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); // Get all notification messages associated with the notification var messages = _notificationMessageRepository.GetAll().Where(m => m.PartOf.Id == notification.Id).ToList(); - while (attempt < MaxRetries && !sentSuccessfully) + while (attempt < MaxRetries && !sentSuccessfully.Item1) { try { @@ -129,7 +129,7 @@ public async Task SendBroadcastAsync(Notification notification, string subject, foreach (var message in messages) { - if (sentSuccessfully) + if (sentSuccessfully.Item1) { // Update the status to Sent for successful messages message.Status = RefListNotificationStatus.Sent; @@ -141,7 +141,7 @@ public async Task SendBroadcastAsync(Notification notification, string subject, Console.WriteLine($"Attempt {attempt + 1} to send notification failed."); message.Status = RefListNotificationStatus.Failed; message.RetryCount = attempt; - message.ErrorMessage = $"Failed to send notification on attempt {attempt + 1}."; + message.ErrorMessage = $"Failed to send notification on attempt {attempt + 1}. Message: {sentSuccessfully.Item2}"; } // Save changes to each message @@ -156,7 +156,7 @@ public async Task SendBroadcastAsync(Notification notification, string subject, } } - if (sentSuccessfully) + if (sentSuccessfully.Item1) { // If any message was successfully sent, exit the loop break; @@ -175,7 +175,7 @@ public async Task SendBroadcastAsync(Notification notification, string subject, } } - if (!sentSuccessfully) + if (!sentSuccessfully.Item1) { Console.WriteLine($"Failed to send notification after {MaxRetries} attempts."); } diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs index 288a0a6659..a3acfbf19b 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs @@ -71,9 +71,9 @@ public async Task SendAsync(Person fromPerson, Person toPerson, Notificati return await gateway.SendAsync(GetRecipientId(toPerson), message.Message); } - public async Task BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) + public async Task> BroadcastAsync(NotificationTopic topic, string subject, string message, List attachments = null) { - return await Task.FromResult(false); + return await Task.FromResult(new Tuple(false, "Broadcast Implementation not yet implemented!")); } } } diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs index 48a4f3f255..dc897bee8a 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationChannelConfig.cs @@ -21,7 +21,7 @@ public NotificationChannelConfig() private void Init() { - VersionStatus = ConfigurationItemVersionStatus.Live; + VersionStatus = ConfigurationItemVersionStatus.Draft; } /// diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs index 9f24e1f113..ae6d95eb8c 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationGatewayConfig.cs @@ -21,7 +21,7 @@ public NotificationGatewayConfig() private void Init() { - VersionStatus = ConfigurationItemVersionStatus.Live; + VersionStatus = ConfigurationItemVersionStatus.Draft; } /// diff --git a/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs b/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs index e63049dd21..58471326ee 100644 --- a/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs +++ b/shesha-core/src/Shesha.Core/Domain/NotificationTypeConfig.cs @@ -21,7 +21,7 @@ public NotificationTypeConfig() private void Init() { - VersionStatus = ConfigurationItemVersionStatus.Live; + VersionStatus = ConfigurationItemVersionStatus.Draft; } /// diff --git a/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs b/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs index 8ebebb081a..6a657ca160 100644 --- a/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs +++ b/shesha-core/src/Shesha.Framework/Configuration/SheshaSettingNames.cs @@ -12,8 +12,6 @@ public static class SheshaSettingNames public const string SmsSettings = "Shesha.SmsSettings"; - public const string OmoSmsSettings = "Shesha.OmoSmsSettings"; - public const string ThemeSettings = "Shesha.ThemeSettings"; public const string MainMenuSettings = "Shesha.MainMenuSettings"; From 51fec6511f9dc8a8f8bfa08084f274dec801acc4 Mon Sep 17 00:00:00 2001 From: Omolemo Lethuloe Date: Thu, 28 Nov 2024 17:18:16 +0200 Subject: [PATCH 5/5] Channels and Factory registration --- .../Emails/EmailChannelSender.cs | 4 +- .../Emails/Gateways/EmailGatewayFactory.cs | 20 +++---- .../Emails/Gateways/IEmailGateway.cs | 1 + .../Emails/Gateways/IEmailGatewayFactory.cs | 13 +++++ .../Emails/Gateways/SmtpGateway.cs | 2 + .../Notifications/NotificationAppService.cs | 55 +++++++++++++++++++ .../Notifications/NotificationSender.cs | 31 +++++++---- .../Sms/Gateways/ClickatellGateway.cs | 2 + .../Notifications/Sms/Gateways/ISmsGateway.cs | 1 + .../Sms/Gateways/ISmsGatewayFactory.cs | 15 +++++ .../Sms/Gateways/SmsGatewayFactory.cs | 21 +++---- .../Notifications/Sms/SmsChannelSender.cs | 4 +- .../src/Shesha.Web.Host/Startup/Startup.cs | 15 +++-- 13 files changed, 144 insertions(+), 40 deletions(-) create mode 100644 shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGatewayFactory.cs create mode 100644 shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGatewayFactory.cs diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs index 1e1f16616a..c62cf2abd8 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/EmailChannelSender.cs @@ -24,12 +24,12 @@ namespace Shesha.Notifications public class EmailChannelSender : INotificationChannelSender { private readonly INotificationSettings _notificationSettings; - private readonly EmailGatewayFactory _emailGatewayFactory; + private readonly IEmailGatewayFactory _emailGatewayFactory; private readonly IRepository _notificationGatewayRepository; private readonly IRepository _userTopicSubscriptionRepository; public ILogger Logger { get; set; } = NullLogger.Instance; - public EmailChannelSender(INotificationSettings notificationSettings, EmailGatewayFactory emailGatewayFactory, IRepository notificationGatewayRepository, IRepository userTopicSubscriptionRepository) + public EmailChannelSender(INotificationSettings notificationSettings, IEmailGatewayFactory emailGatewayFactory, IRepository notificationGatewayRepository, IRepository userTopicSubscriptionRepository) { _notificationSettings = notificationSettings; _userTopicSubscriptionRepository = userTopicSubscriptionRepository; diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs index 079d5e0eea..60b16e5d3c 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/EmailGatewayFactory.cs @@ -7,23 +7,23 @@ namespace Shesha.Notifications.Emails.Gateways { - public class EmailGatewayFactory + public class EmailGatewayFactory: IEmailGatewayFactory { - private readonly IServiceProvider _serviceProvider; + private readonly IEnumerable _emailGateways; - public EmailGatewayFactory(IServiceProvider serviceProvider) + public EmailGatewayFactory(IEnumerable emailGateways) { - _serviceProvider = serviceProvider; + _emailGateways = emailGateways; } public IEmailGateway GetGateway(string gatewayName) { - return gatewayName switch - { - "SmtpGateway" => _serviceProvider.GetService(), - // Add other gateways here - _ => throw new NotSupportedException($"Gateway {gatewayName} is not supported.") - }; + var gateway = _emailGateways.FirstOrDefault(g => g.Name.Equals(gatewayName, StringComparison.OrdinalIgnoreCase)); + + if (gateway == null) + throw new NotSupportedException($"Gateway '{gatewayName}' is not supported. Available gateways: {string.Join(", ", _emailGateways.Select(g => g.Name))}"); + + return gateway; } } } diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs index 316b20c5d0..067ff764db 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGateway.cs @@ -10,6 +10,7 @@ namespace Shesha.Notifications.Emails.Gateways { public interface IEmailGateway { + string Name { get;} Task SendAsync(string fromPerson, string toPerson, NotificationMessage message, bool isBodyHtml, string cc = "", bool throwException = false, List attachments = null); Task> BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); } diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGatewayFactory.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGatewayFactory.cs new file mode 100644 index 0000000000..663802464e --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/IEmailGatewayFactory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Notifications.Emails.Gateways +{ + public interface IEmailGatewayFactory + { + IEmailGateway GetGateway(string gatewayName); + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs index 16c115f35b..0dcbff5858 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Emails/Gateways/SmtpGateway.cs @@ -17,6 +17,8 @@ namespace Shesha.Notifications.Emails.Gateways { public class SmtpGateway : IEmailGateway { + public string Name => "SmtpGateway"; + private readonly IEmailGatewaySettings _emailGatewaySettings; private readonly INotificationSettings _notificationSettings; diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs index e363dfc072..a91b5f3b6c 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationAppService.cs @@ -72,6 +72,61 @@ public NotificationAppService(IEnumerable channelSen _notificationTopicRepository = notificationTopicRepository; } + public class TestData : NotificationData + { + public string subject { get; set; } + public string name { get; set; } + public string body { get; set; } + } + + public async Task>> OmoTest() + { + var type = await _notificationTypeRepository.FirstOrDefaultAsync(x => x.Name == "Warning"); + var fromPerson = await _personRepository.FirstOrDefaultAsync(x => x.FirstName == "System"); + var toPerson = await _personRepository.FirstOrDefaultAsync(x => x.EmailAddress1 == "omolemo.lethuloe@boxfusion.io"); + var channel = await _notificationChannelRepository.FirstOrDefaultAsync(x => x.Name == "SMS"); + var getAttachments = await _storedFileService.GetAttachmentsAsync(fromPerson.Id, "Shesha.Domain.Person"); + + var attachments = getAttachments.Select(x => new NotificationAttachmentDto() + { + FileName = x.FileName, + StoredFileId = x.Id, + }).ToList(); + + + var testing = new TestData() + { + name = "Omolemo", + subject = "Test Subject", + body = "Test Body" + }; + var triggeringEntity = new GenericEntityReference(fromPerson); + return await SendNotification(type, fromPerson, toPerson, data: testing, RefListNotificationPriority.High, attachments, triggeringEntity, channel); + } + + public async Task>> OmoBroadcastTest() + { + var type = await _notificationTypeRepository.FirstOrDefaultAsync(x => x.Name == "Warning"); + var topic = await _notificationTopicRepository.FirstOrDefaultAsync(x => x.Name == "Service Requests"); + var getAttachments = await _storedFileService.GetAttachmentsAsync(topic.Id, "Shesha.Core.NotificationTopic"); + + var attachments = getAttachments.Select(x => new NotificationAttachmentDto() + { + FileName = x.FileName, + StoredFileId = x.Id, + }).ToList(); + + + var testing = new TestData() + { + name = "Omolemo", + subject = "Test Subject", + body = "Test Body" + }; + var triggeringEntity = new GenericEntityReference(topic); + return await SendBroadcastNotification(type, topic, data: testing, RefListNotificationPriority.High, attachments, triggeringEntity); + } + /// /// /// diff --git a/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs index 03912075ae..c1fd4b4f10 100644 --- a/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/NotificationSender.cs @@ -23,19 +23,34 @@ namespace Shesha.Notifications public class NotificationSender: INotificationSender { private readonly INotificationChannelSender _channelSender; + private readonly IIocManager _iocManager; + private readonly IRepository _notificationMessageRepository; + private readonly IRepository _attachmentRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly ISessionProvider _sessionProvider; + private readonly IStoredFileService _fileService; public readonly int MaxRetries = 3; - public NotificationSender(INotificationChannelSender channelSender) + public NotificationSender(INotificationChannelSender channelSender, + IIocManager iocManager, + IRepository notificationMessageRepository, + IRepository attachmentRepository, + IUnitOfWorkManager unitOfWorkManager, + ISessionProvider sessionProvider, + IStoredFileService fileService) { _channelSender = channelSender; + _iocManager = iocManager; + _notificationMessageRepository = notificationMessageRepository; + _attachmentRepository = attachmentRepository; + _unitOfWorkManager = unitOfWorkManager; + _sessionProvider = sessionProvider; + _fileService = fileService; } private async Task> GetAttachmentsAsync(NotificationMessage message) { - var _attachmentRepository = StaticContext.IocManager.Resolve>(); - var _fileService = StaticContext.IocManager.Resolve(); - var attachments = await _attachmentRepository.GetAll().Where(a => a.Message.Id == message.Id).ToListAsync(); var result = attachments.Select(a => new EmailAttachment(a.FileName, _fileService.GetStream(a.File))).ToList(); @@ -46,9 +61,6 @@ private async Task> GetAttachmentsAsync(NotificationMessag [UnitOfWork] public async Task SendAsync(Person fromPerson, Person toPerson, NotificationMessage message, bool isBodyHtml) { - var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); - var _unitOfWorkManager = StaticContext.IocManager.Resolve(); - var _sessionProvider = StaticContext.IocManager.Resolve(); var attachments = await GetAttachmentsAsync(message); int attempt = 0; bool sentSuccessfully = false; @@ -115,7 +127,6 @@ public async Task SendBroadcastAsync(Notification notification, string subject, { int attempt = 0; Tuple sentSuccessfully = new Tuple(false, ""); - var _notificationMessageRepository = StaticContext.IocManager.Resolve>(); // Get all notification messages associated with the notification var messages = _notificationMessageRepository.GetAll().Where(m => m.PartOf.Id == notification.Id).ToList(); @@ -145,9 +156,9 @@ public async Task SendBroadcastAsync(Notification notification, string subject, } // Save changes to each message - using (var uow = StaticContext.IocManager.Resolve().Begin()) + using (var uow = _unitOfWorkManager.Begin()) { - using (var transaction = StaticContext.IocManager.Resolve().Session.BeginTransaction()) + using (var transaction = _sessionProvider.Session.BeginTransaction()) { await _notificationMessageRepository.UpdateAsync(message); transaction.Commit(); diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs index 3a25fe5b05..5148b18f6c 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ClickatellGateway.cs @@ -16,6 +16,8 @@ namespace Shesha.Notifications.Sms.Gateways { public class ClickatellGateway: ISmsGateway { + public string Name => "ClickatellGateway"; + private readonly ISmsGatewaySettings _smsGatewaySettings; private readonly INotificationSettings _notificationSettings; public ClickatellGateway(ISmsGatewaySettings smsGatewaySettings, INotificationSettings notificationSettings) diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs index aa92672d57..a0f70527a4 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGateway.cs @@ -10,6 +10,7 @@ namespace Shesha.Notifications.Sms.Gateways { public interface ISmsGateway { + string Name { get; } Task SendAsync(string fromPerson, string message); Task BroadcastAsync(string topicSubscribers, string subject, string message, List attachments = null); } diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGatewayFactory.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGatewayFactory.cs new file mode 100644 index 0000000000..c7bc1d3b6b --- /dev/null +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/ISmsGatewayFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Shesha.Notifications.Emails.Gateways; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shesha.Notifications.Sms.Gateways +{ + public interface ISmsGatewayFactory + { + ISmsGateway GetGateway(string gatewayName); + } +} diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs index 1e33eea75c..ae2f3f469f 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/Gateways/SmsGatewayFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Shesha.Notifications.Emails.Gateways; using System; using System.Collections.Generic; using System.Linq; @@ -7,23 +8,23 @@ namespace Shesha.Notifications.Sms.Gateways { - public class SmsGatewayFactory + public class SmsGatewayFactory: ISmsGatewayFactory { - private readonly IServiceProvider _serviceProvider; + private readonly IEnumerable _smsGateways; - public SmsGatewayFactory(IServiceProvider serviceProvider) + public SmsGatewayFactory(IEnumerable smsGateways) { - _serviceProvider = serviceProvider; + _smsGateways = smsGateways; } public ISmsGateway GetGateway(string gatewayName) { - return gatewayName switch - { - "ClickatellGateway" => _serviceProvider.GetService(), - // Add other gateways here - _ => throw new NotSupportedException($"Gateway {gatewayName} is not supported.") - }; + var gateway = _smsGateways.FirstOrDefault(g => g.Name.Equals(gatewayName, StringComparison.OrdinalIgnoreCase)); + + if (gateway == null) + throw new NotSupportedException($"Gateway '{gatewayName}' is not supported. Available gateways: {string.Join(", ", _smsGateways.Select(g => g.Name))}"); + + return gateway; } } } diff --git a/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs index a3acfbf19b..02152132c7 100644 --- a/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs +++ b/shesha-core/src/Shesha.Application/Notifications/Sms/SmsChannelSender.cs @@ -26,11 +26,11 @@ public class SmsChannelSender : INotificationChannelSender { private readonly INotificationSettings _notificationSettings; private readonly IRepository _notificationGatewayRepository; - private readonly SmsGatewayFactory _smsGatewayFactory; + private readonly ISmsGatewayFactory _smsGatewayFactory; public ILogger Logger { get; set; } = NullLogger.Instance; - public SmsChannelSender(INotificationSettings notificationSettings, SmsGatewayFactory smsGatewayFactory, IRepository notificationGatewayRepository) + public SmsChannelSender(INotificationSettings notificationSettings, ISmsGatewayFactory smsGatewayFactory, IRepository notificationGatewayRepository) { _notificationSettings = notificationSettings; _smsGatewayFactory = smsGatewayFactory; diff --git a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs index 66c8cfc992..9a96783279 100644 --- a/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs +++ b/shesha-core/src/Shesha.Web.Host/Startup/Startup.cs @@ -61,6 +61,8 @@ public Startup(IWebHostEnvironment hostEnvironment) public IServiceProvider ConfigureServices(IServiceCollection services) { + services.AddTransient(); + // Should be before AddMvcCore services.AddSingleton(SheshaActionDescriptorChangeProvider.Instance); services.AddSingleton(SheshaActionDescriptorChangeProvider.Instance); @@ -100,12 +102,13 @@ public IServiceProvider ConfigureServices(IServiceCollection services) IdentityRegistrar.Register(services); AuthConfigurer.Configure(services, _appConfiguration); - services.AddScoped(); - services.AddScoped(); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddSignalR();