From 1fd6b5a99676c4f311da8e99a498e03e39c6e78f Mon Sep 17 00:00:00 2001 From: cKey <35512826+colinin@users.noreply.github.com> Date: Sat, 27 Aug 2022 13:52:51 +0800 Subject: [PATCH] feat: optimize push service --- aspnet-core/LINGYUN.MicroService.All.sln | 57 ++++- aspnet-core/LINGYUN.MicroService.Common.sln | 25 ++- .../Notifications/AbpNotificationModule.cs | 2 + .../INotificationPublishProvider.cs | 5 +- .../NotificationPublishProvider.cs | 22 +- .../NotificationDataExtensions.cs | 33 +++ .../NotificationDefinitionExtensions.cs | 43 +++- .../PushPlusNotificationPublishProvider.cs | 30 +++ .../PushPlusFeatureDefinitionProvider.cs | 10 +- ...tMiniProgramNotificationPublishProvider.cs | 20 +- .../User/IdentityWxPusherUserStore.cs | 31 ++- .../FodyWeavers.xml | 3 + .../FodyWeavers.xsd | 30 +++ .../NotificationDataExtensions.cs | 19 ++ .../NotificationDefinitionExtensions.cs | 29 +++ .../AbpNotificationsWxPusherModule.cs | 2 +- .../WxPusherNotificationPublishProvider.cs | 33 ++- .../WxPusherFeatureDefinitionProvider.cs | 4 +- .../Security/Claims/AbpWxPusherClaimTypes.cs | 6 +- .../Abp/WxPusher/User/IWxPusherUserStore.cs | 9 + .../WxPusher/User/NullWxPusherUserStore.cs | 7 + ...ervice.RealtimeMessage.HttpApi.Host.csproj | 2 + .../RealtimeMessageHttpApiHostModule.cs | 4 + .../Notifications/FakeNotificationSender.cs | 205 ++++++++++++++++++ .../Notifications/NotificationsTestsNames.cs | 2 +- ...UN.Abp.Notifications.WxPusher.Tests.csproj | 20 ++ .../AbpNotificationsWxPusherTestBase.cs | 7 + .../AbpNotificationsWxPusherTestModule.cs | 12 + .../WxPusher/NotificationSenderTests.cs | 77 +++++++ ...icationsWxPusherTestsDefinitionProvider.cs | 28 +++ .../Usings.cs | 2 + 31 files changed, 750 insertions(+), 29 deletions(-) create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs create mode 100644 aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xml create mode 100644 aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xsd create mode 100644 aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/FakeNotificationSender.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN.Abp.Notifications.WxPusher.Tests.csproj create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestBase.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestModule.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationSenderTests.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationsWxPusherTestsDefinitionProvider.cs create mode 100644 aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/Usings.cs diff --git a/aspnet-core/LINGYUN.MicroService.All.sln b/aspnet-core/LINGYUN.MicroService.All.sln index 6958e012f..79bda6c14 100644 --- a/aspnet-core/LINGYUN.MicroService.All.sln +++ b/aspnet-core/LINGYUN.MicroService.All.sln @@ -442,7 +442,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.CachingManageme EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.CachingManagement.HttpApi", "modules\caching\LINGYUN.Abp.CachingManagement.HttpApi\LINGYUN.Abp.CachingManagement.HttpApi.csproj", "{B507D18B-770E-4581-854B-15579AC7074F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.DistributedLocking.Dapr", "modules\dapr\LINGYUN.Abp.DistributedLocking.Dapr\LINGYUN.Abp.DistributedLocking.Dapr.csproj", "{178AF3DE-1C24-41A9-8CA0-64A7D85DDFFE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.DistributedLocking.Dapr", "modules\dapr\LINGYUN.Abp.DistributedLocking.Dapr\LINGYUN.Abp.DistributedLocking.Dapr.csproj", "{178AF3DE-1C24-41A9-8CA0-64A7D85DDFFE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "wx-pusher", "wx-pusher", "{53DF60C5-AE45-4FCE-9C9B-EE15F0E33BD4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WxPusher", "modules\wx-pusher\LINGYUN.Abp.WxPusher\LINGYUN.Abp.WxPusher.csproj", "{7C4C266C-DC2A-4A48-AB87-185E284B86E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.WxPusher", "modules\wx-pusher\LINGYUN.Abp.Notifications.WxPusher\LINGYUN.Abp.Notifications.WxPusher.csproj", "{0380623A-EC74-430B-AA59-A6D23BC5E108}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WxPusher.SettingManagement", "modules\wx-pusher\LINGYUN.Abp.WxPusher.SettingManagement\LINGYUN.Abp.WxPusher.SettingManagement.csproj", "{C6BDAB62-0B82-4ED2-8455-2FEA1F1B1EAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.WxPusher", "modules\wx-pusher\LINGYUN.Abp.Identity.WxPusher\LINGYUN.Abp.Identity.WxPusher.csproj", "{C9BB4BB2-97B8-4CDE-B961-2F0A4CB5D7B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pushplus", "pushplus", "{A8F1C9FA-4F16-43F9-8CC6-CCD124154440}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.PushPlus", "modules\pushplus\LINGYUN.Abp.PushPlus\LINGYUN.Abp.PushPlus.csproj", "{82CB7E17-ED5A-4D26-B116-BA7B4226E43C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.PushPlus.SettingManagement", "modules\pushplus\LINGYUN.Abp.PushPlus.SettingManagement\LINGYUN.Abp.PushPlus.SettingManagement.csproj", "{102C3711-135E-446D-9A35-AEFB79993CAA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.PushPlus", "modules\pushplus\LINGYUN.Abp.Notifications.PushPlus\LINGYUN.Abp.Notifications.PushPlus.csproj", "{2B7C1441-8801-4121-ABFB-03771E9DFE46}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1154,6 +1172,34 @@ Global {178AF3DE-1C24-41A9-8CA0-64A7D85DDFFE}.Debug|Any CPU.Build.0 = Debug|Any CPU {178AF3DE-1C24-41A9-8CA0-64A7D85DDFFE}.Release|Any CPU.ActiveCfg = Release|Any CPU {178AF3DE-1C24-41A9-8CA0-64A7D85DDFFE}.Release|Any CPU.Build.0 = Release|Any CPU + {7C4C266C-DC2A-4A48-AB87-185E284B86E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C4C266C-DC2A-4A48-AB87-185E284B86E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C4C266C-DC2A-4A48-AB87-185E284B86E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C4C266C-DC2A-4A48-AB87-185E284B86E9}.Release|Any CPU.Build.0 = Release|Any CPU + {0380623A-EC74-430B-AA59-A6D23BC5E108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0380623A-EC74-430B-AA59-A6D23BC5E108}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0380623A-EC74-430B-AA59-A6D23BC5E108}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0380623A-EC74-430B-AA59-A6D23BC5E108}.Release|Any CPU.Build.0 = Release|Any CPU + {C6BDAB62-0B82-4ED2-8455-2FEA1F1B1EAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6BDAB62-0B82-4ED2-8455-2FEA1F1B1EAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6BDAB62-0B82-4ED2-8455-2FEA1F1B1EAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6BDAB62-0B82-4ED2-8455-2FEA1F1B1EAD}.Release|Any CPU.Build.0 = Release|Any CPU + {C9BB4BB2-97B8-4CDE-B961-2F0A4CB5D7B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9BB4BB2-97B8-4CDE-B961-2F0A4CB5D7B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9BB4BB2-97B8-4CDE-B961-2F0A4CB5D7B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9BB4BB2-97B8-4CDE-B961-2F0A4CB5D7B1}.Release|Any CPU.Build.0 = Release|Any CPU + {82CB7E17-ED5A-4D26-B116-BA7B4226E43C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82CB7E17-ED5A-4D26-B116-BA7B4226E43C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82CB7E17-ED5A-4D26-B116-BA7B4226E43C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82CB7E17-ED5A-4D26-B116-BA7B4226E43C}.Release|Any CPU.Build.0 = Release|Any CPU + {102C3711-135E-446D-9A35-AEFB79993CAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {102C3711-135E-446D-9A35-AEFB79993CAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {102C3711-135E-446D-9A35-AEFB79993CAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {102C3711-135E-446D-9A35-AEFB79993CAA}.Release|Any CPU.Build.0 = Release|Any CPU + {2B7C1441-8801-4121-ABFB-03771E9DFE46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B7C1441-8801-4121-ABFB-03771E9DFE46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B7C1441-8801-4121-ABFB-03771E9DFE46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B7C1441-8801-4121-ABFB-03771E9DFE46}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1371,6 +1417,15 @@ Global {08CC528E-98D7-41D9-957D-9F9064645788} = {63FCC71F-1CEF-44D3-B95B-23EE58DE8C95} {B507D18B-770E-4581-854B-15579AC7074F} = {63FCC71F-1CEF-44D3-B95B-23EE58DE8C95} {178AF3DE-1C24-41A9-8CA0-64A7D85DDFFE} = {DC33925B-264D-421B-96CC-46F853CBCC70} + {53DF60C5-AE45-4FCE-9C9B-EE15F0E33BD4} = {C5CAD011-DF84-4914-939C-0C029DCEF26F} + {7C4C266C-DC2A-4A48-AB87-185E284B86E9} = {53DF60C5-AE45-4FCE-9C9B-EE15F0E33BD4} + {0380623A-EC74-430B-AA59-A6D23BC5E108} = {53DF60C5-AE45-4FCE-9C9B-EE15F0E33BD4} + {C6BDAB62-0B82-4ED2-8455-2FEA1F1B1EAD} = {53DF60C5-AE45-4FCE-9C9B-EE15F0E33BD4} + {C9BB4BB2-97B8-4CDE-B961-2F0A4CB5D7B1} = {53DF60C5-AE45-4FCE-9C9B-EE15F0E33BD4} + {A8F1C9FA-4F16-43F9-8CC6-CCD124154440} = {C5CAD011-DF84-4914-939C-0C029DCEF26F} + {82CB7E17-ED5A-4D26-B116-BA7B4226E43C} = {A8F1C9FA-4F16-43F9-8CC6-CCD124154440} + {102C3711-135E-446D-9A35-AEFB79993CAA} = {A8F1C9FA-4F16-43F9-8CC6-CCD124154440} + {2B7C1441-8801-4121-ABFB-03771E9DFE46} = {A8F1C9FA-4F16-43F9-8CC6-CCD124154440} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718} diff --git a/aspnet-core/LINGYUN.MicroService.Common.sln b/aspnet-core/LINGYUN.MicroService.Common.sln index 43f6a5e48..6167bb21c 100644 --- a/aspnet-core/LINGYUN.MicroService.Common.sln +++ b/aspnet-core/LINGYUN.MicroService.Common.sln @@ -260,9 +260,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.WxPusher", "modules\wx-pusher\LINGYUN.Abp.Identity.WxPusher\LINGYUN.Abp.Identity.WxPusher.csproj", "{30FA01ED-921A-4E7D-9E83-6719538FB866}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.WxPusher.SettingManagement", "modules\wx-pusher\LINGYUN.Abp.WxPusher.SettingManagement\LINGYUN.Abp.WxPusher.SettingManagement.csproj", "{E6FDAD7B-4A7F-4CEB-9891-F8FC4E556C36}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WxPusher.SettingManagement", "modules\wx-pusher\LINGYUN.Abp.WxPusher.SettingManagement\LINGYUN.Abp.WxPusher.SettingManagement.csproj", "{E6FDAD7B-4A7F-4CEB-9891-F8FC4E556C36}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.PushPlus.SettingManagement", "modules\pushplus\LINGYUN.Abp.PushPlus.SettingManagement\LINGYUN.Abp.PushPlus.SettingManagement.csproj", "{4CBB4A0C-6D23-44DD-BAF4-49D69ED35839}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.PushPlus.SettingManagement", "modules\pushplus\LINGYUN.Abp.PushPlus.SettingManagement\LINGYUN.Abp.PushPlus.SettingManagement.csproj", "{4CBB4A0C-6D23-44DD-BAF4-49D69ED35839}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Notifications.WxPusher.Tests", "tests\LINGYUN.Abp.Notifications.WxPusher.Tests\LINGYUN.Abp.Notifications.WxPusher.Tests.csproj", "{C2801414-550F-4A25-AD8D-68AC508211DC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.Tests", "tests\LINGYUN.Abp.Notifications.Tests\LINGYUN.Abp.Notifications.Tests.csproj", "{868A1718-4970-48D2-A256-08EF468302D8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.WeChat.MiniProgram.Tests", "tests\LINGYUN.Abp.Notifications.WeChat.MiniProgram.Tests\LINGYUN.Abp.Notifications.WeChat.MiniProgram.Tests.csproj", "{B78E53AC-6BB8-402D-90CF-BEF1BD9558EB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -678,6 +684,18 @@ Global {4CBB4A0C-6D23-44DD-BAF4-49D69ED35839}.Debug|Any CPU.Build.0 = Debug|Any CPU {4CBB4A0C-6D23-44DD-BAF4-49D69ED35839}.Release|Any CPU.ActiveCfg = Release|Any CPU {4CBB4A0C-6D23-44DD-BAF4-49D69ED35839}.Release|Any CPU.Build.0 = Release|Any CPU + {C2801414-550F-4A25-AD8D-68AC508211DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2801414-550F-4A25-AD8D-68AC508211DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2801414-550F-4A25-AD8D-68AC508211DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2801414-550F-4A25-AD8D-68AC508211DC}.Release|Any CPU.Build.0 = Release|Any CPU + {868A1718-4970-48D2-A256-08EF468302D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {868A1718-4970-48D2-A256-08EF468302D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {868A1718-4970-48D2-A256-08EF468302D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {868A1718-4970-48D2-A256-08EF468302D8}.Release|Any CPU.Build.0 = Release|Any CPU + {B78E53AC-6BB8-402D-90CF-BEF1BD9558EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B78E53AC-6BB8-402D-90CF-BEF1BD9558EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B78E53AC-6BB8-402D-90CF-BEF1BD9558EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B78E53AC-6BB8-402D-90CF-BEF1BD9558EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -809,6 +827,9 @@ Global {30FA01ED-921A-4E7D-9E83-6719538FB866} = {7862CE70-76EF-4228-A703-C2E2A9704D14} {E6FDAD7B-4A7F-4CEB-9891-F8FC4E556C36} = {7862CE70-76EF-4228-A703-C2E2A9704D14} {4CBB4A0C-6D23-44DD-BAF4-49D69ED35839} = {0F5A2591-CE08-4184-A5F3-89F6FB3B2B10} + {C2801414-550F-4A25-AD8D-68AC508211DC} = {B86C21A4-73B7-471E-B73A-B4B905EC9435} + {868A1718-4970-48D2-A256-08EF468302D8} = {B86C21A4-73B7-471E-B73A-B4B905EC9435} + {B78E53AC-6BB8-402D-90CF-BEF1BD9558EB} = {B86C21A4-73B7-471E-B73A-B4B905EC9435} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {06C707C6-02C0-411A-AD3B-2D0E13787CB8} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/AbpNotificationModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/AbpNotificationModule.cs index 730db1bc3..8f7e43a2e 100644 --- a/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/AbpNotificationModule.cs +++ b/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/AbpNotificationModule.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using Volo.Abp.BackgroundJobs; using Volo.Abp.BackgroundWorkers; +using Volo.Abp.EventBus; using Volo.Abp.Json; using Volo.Abp.Json.SystemTextJson; using Volo.Abp.Localization; @@ -23,6 +24,7 @@ namespace LINGYUN.Abp.Notifications typeof(AbpJsonModule), typeof(AbpLocalizationModule), typeof(AbpRealTimeModule), + typeof(AbpEventBusModule), typeof(AbpTextTemplatingCoreModule))] public class AbpNotificationModule : AbpModule { diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/INotificationPublishProvider.cs b/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/INotificationPublishProvider.cs index b4c84068f..95901fc42 100644 --- a/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/INotificationPublishProvider.cs +++ b/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/INotificationPublishProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace LINGYUN.Abp.Notifications @@ -18,6 +19,8 @@ namespace LINGYUN.Abp.Notifications /// 通知信息 /// 接收用户列表 /// - Task PublishAsync(NotificationInfo notification, IEnumerable identifiers); + Task PublishAsync( + NotificationInfo notification, + IEnumerable identifiers); } } diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/NotificationPublishProvider.cs b/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/NotificationPublishProvider.cs index b5679c4e2..e6c23c109 100644 --- a/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/NotificationPublishProvider.cs +++ b/aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/NotificationPublishProvider.cs @@ -22,9 +22,27 @@ namespace LINGYUN.Abp.Notifications public ICancellationTokenProvider CancellationTokenProvider => ServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); - public async Task PublishAsync(NotificationInfo notification, IEnumerable identifiers) + public async Task PublishAsync( + NotificationInfo notification, + IEnumerable identifiers) { - await PublishAsync(notification, identifiers, CancellationTokenProvider.Token); + if (await CanPublishAsync(notification)) + { + await PublishAsync( + notification, + identifiers, + GetCancellationToken()); + } + } + protected virtual Task CanPublishAsync( + NotificationInfo notification, + CancellationToken cancellationToken = default) + { + return Task.FromResult(true); + } + protected virtual CancellationToken GetCancellationToken(CancellationToken cancellationToken = default) + { + return CancellationTokenProvider.FallbackToProvider(cancellationToken); } /// /// 重写实现通知发布 diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs new file mode 100644 index 000000000..defe147ca --- /dev/null +++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs @@ -0,0 +1,33 @@ +namespace LINGYUN.Abp.Notifications; +public static class NotificationDataExtensions +{ + private const string Prefix = "push-plus:"; + private const string WebhookKey = Prefix + "webhook"; + private const string CallbackUrlKey = Prefix + "callback"; + + public static void SetWebhook( + this NotificationData notificationData, + string url) + { + notificationData.TrySetData(WebhookKey, url); + } + + public static string GetWebhookOrNull( + this NotificationData notificationData) + { + return notificationData.TryGetData(WebhookKey)?.ToString(); + } + + public static void SetCallbackUrl( + this NotificationData notificationData, + string callbackUrl) + { + notificationData.TrySetData(CallbackUrlKey, callbackUrl); + } + + public static string GetCallbackUrlOrNull( + this NotificationData notificationData) + { + return notificationData.TryGetData(CallbackUrlKey)?.ToString(); + } +} diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs index 765a2da2d..a2feca472 100644 --- a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs +++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs @@ -1,9 +1,44 @@ using LINGYUN.Abp.PushPlus.Channel; +using LINGYUN.Abp.PushPlus.Message; using System; namespace LINGYUN.Abp.Notifications; public static class NotificationDefinitionExtensions { + private const string Prefix = "push-plus:"; + private const string TemplateKey = Prefix + "template"; + private const string ChannelTypeKey = Prefix + "channel"; + private const string TopicKey = Prefix + "topic"; + /// + /// 设定消息模板 + /// + /// + /// + /// + public static NotificationDefinition WithTemplate( + this NotificationDefinition notification, + PushPlusMessageTemplate template = PushPlusMessageTemplate.Text) + { + return notification.WithProperty(TemplateKey, template); + } + /// + /// 获取消息模板 + /// + /// + /// + /// + public static PushPlusMessageTemplate GetTemplateOrDefault( + this NotificationDefinition notification, + PushPlusMessageTemplate defaultTemplate = PushPlusMessageTemplate.Text) + { + if (notification.Properties.TryGetValue(TemplateKey, out var defineTemplate) == true && + defineTemplate is PushPlusMessageTemplate template) + { + return template; + } + + return defaultTemplate; + } /// /// 设定消息发送通道 /// @@ -14,7 +49,7 @@ public static class NotificationDefinitionExtensions this NotificationDefinition notification, PushPlusChannelType channelType) { - return notification.WithProperty("channel", channelType); + return notification.WithProperty(ChannelTypeKey, channelType); } /// /// 获取消息发送通道 @@ -26,7 +61,7 @@ public static class NotificationDefinitionExtensions this NotificationDefinition notification, PushPlusChannelType defaultChannelType = PushPlusChannelType.WeChat) { - if (notification.Properties.TryGetValue("channel", out var defineChannelType) == true && + if (notification.Properties.TryGetValue(ChannelTypeKey, out var defineChannelType) == true && defineChannelType is PushPlusChannelType channelType) { return channelType; @@ -47,7 +82,7 @@ public static class NotificationDefinitionExtensions this NotificationDefinition notification, string topic) { - return notification.WithProperty("topic", topic); + return notification.WithProperty(TopicKey, topic); } /// /// 获取消息群发群组编码 @@ -59,7 +94,7 @@ public static class NotificationDefinitionExtensions public static string GetTopicOrNull( this NotificationDefinition notification) { - if (notification.Properties.TryGetValue("topic", out var topicDefine) == true) + if (notification.Properties.TryGetValue(TopicKey, out var topicDefine) == true) { return topicDefine.ToString(); } diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs index 1e1c75c97..4fc1746cc 100644 --- a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs +++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs @@ -1,13 +1,16 @@ using LINGYUN.Abp.PushPlus.Channel; +using LINGYUN.Abp.PushPlus.Features; using LINGYUN.Abp.PushPlus.Message; using LINGYUN.Abp.RealTime.Localization; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.Features; using Volo.Abp.Localization; namespace LINGYUN.Abp.Notifications.PushPlus; @@ -18,6 +21,8 @@ public class PushPlusNotificationPublishProvider : NotificationPublishProvider public override string Name => ProviderName; + protected IFeatureChecker FeatureChecker { get; } + protected IPushPlusMessageSender PushPlusMessageSender { get; } protected IStringLocalizerFactory LocalizerFactory { get; } @@ -27,17 +32,32 @@ public class PushPlusNotificationPublishProvider : NotificationPublishProvider protected INotificationDefinitionManager NotificationDefinitionManager { get; } public PushPlusNotificationPublishProvider( + IFeatureChecker featureChecker, IPushPlusMessageSender pushPlusMessageSender, IStringLocalizerFactory localizerFactory, IOptions localizationOptions, INotificationDefinitionManager notificationDefinitionManager) { + FeatureChecker = featureChecker; PushPlusMessageSender = pushPlusMessageSender; LocalizerFactory = localizerFactory; LocalizationOptions = localizationOptions.Value; NotificationDefinitionManager = notificationDefinitionManager; } + protected async override Task CanPublishAsync(NotificationInfo notification, CancellationToken cancellationToken = default) + { + if (!await FeatureChecker.IsEnabledAsync(PushPlusFeatureNames.Message.Enable)) + { + Logger.LogWarning( + "{0} cannot push messages because the feature {0} is not enabled", + Name, + PushPlusFeatureNames.Message.Enable); + return false; + } + return true; + } + protected async override Task PublishAsync( NotificationInfo notification, IEnumerable identifiers, @@ -53,6 +73,10 @@ public class PushPlusNotificationPublishProvider : NotificationPublishProvider } var channel = notificationDefine?.GetChannelOrDefault(PushPlusChannelType.Email) ?? PushPlusChannelType.Email; + var template = notificationDefine?.GetTemplateOrDefault(PushPlusMessageTemplate.Text) + ?? PushPlusMessageTemplate.Text; + var webhook = notification.Data.GetWebhookOrNull() ?? ""; + var callbackUrl = notification.Data.GetCallbackUrlOrNull() ?? ""; if (!notification.Data.NeedLocalizer()) { @@ -64,6 +88,9 @@ public class PushPlusNotificationPublishProvider : NotificationPublishProvider message, topic, channelType: channel, + template: template, + webhook: webhook, + callbackUrl: callbackUrl, cancellationToken: cancellationToken); } else @@ -81,6 +108,9 @@ public class PushPlusNotificationPublishProvider : NotificationPublishProvider message, topic, channelType: channel, + template: template, + webhook: webhook, + callbackUrl: callbackUrl, cancellationToken: cancellationToken); } } diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs index 57797b2eb..14c675e82 100644 --- a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs +++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs @@ -14,7 +14,7 @@ public class PushPlusFeatureDefinitionProvider : FeatureDefinitionProvider group.AddFeature( name: PushPlusFeatureNames.Message.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:MessageEnable"), description: L("Features:MessageEnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); @@ -35,7 +35,7 @@ public class PushPlusFeatureDefinitionProvider : FeatureDefinitionProvider description: L("Features:Channel.WeChat")); weChatChannel.CreateChild( name: PushPlusFeatureNames.Channel.WeChat.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:Channel.WeChat.Enable"), description: L("Features:Channel.WeChat.EnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); @@ -62,7 +62,7 @@ public class PushPlusFeatureDefinitionProvider : FeatureDefinitionProvider description: L("Features:Channel.WeWork")); weWorkChannel.CreateChild( name: PushPlusFeatureNames.Channel.WeWork.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:Channel.WeWork.Enable"), description: L("Features:Channel.WeWork.EnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); @@ -89,7 +89,7 @@ public class PushPlusFeatureDefinitionProvider : FeatureDefinitionProvider description: L("Features:Channel.Webhook")); webhookChannel.CreateChild( name: PushPlusFeatureNames.Channel.Webhook.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:Channel.Webhook.Enable"), description: L("Features:Channel.Webhook.EnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); @@ -116,7 +116,7 @@ public class PushPlusFeatureDefinitionProvider : FeatureDefinitionProvider description: L("Features:Channel.Email")); emailChannel.CreateChild( name: PushPlusFeatureNames.Channel.Email.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:Channel.Email.Enable"), description: L("Features:Channel.Email.EnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); diff --git a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN/Abp/Notifications/WeChat/MiniProgram/WeChatMiniProgramNotificationPublishProvider.cs b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN/Abp/Notifications/WeChat/MiniProgram/WeChatMiniProgramNotificationPublishProvider.cs index 169c8d6a5..8548aa9ea 100644 --- a/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN/Abp/Notifications/WeChat/MiniProgram/WeChatMiniProgramNotificationPublishProvider.cs +++ b/aspnet-core/modules/wechat/LINGYUN.Abp.Notifications.WeChat.MiniProgram/LINGYUN/Abp/Notifications/WeChat/MiniProgram/WeChatMiniProgramNotificationPublishProvider.cs @@ -1,4 +1,5 @@ -using LINGYUN.Abp.WeChat.MiniProgram.Messages; +using LINGYUN.Abp.WeChat.MiniProgram.Features; +using LINGYUN.Abp.WeChat.MiniProgram.Messages; using LINGYUN.Abp.WeChat.Security.Claims; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -6,6 +7,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.Features; namespace LINGYUN.Abp.Notifications.WeChat.MiniProgram { @@ -16,16 +18,32 @@ namespace LINGYUN.Abp.Notifications.WeChat.MiniProgram { public const string ProviderName = NotificationProviderNames.WechatMiniProgram; public override string Name => ProviderName; + protected IFeatureChecker FeatureChecker { get; } protected ISubscribeMessager SubscribeMessager { get; } protected AbpNotificationsWeChatMiniProgramOptions Options { get; } public WeChatMiniProgramNotificationPublishProvider( + IFeatureChecker featureChecker, ISubscribeMessager subscribeMessager, IOptions options) { Options = options.Value; + FeatureChecker = featureChecker; SubscribeMessager = subscribeMessager; } + protected async override Task CanPublishAsync(NotificationInfo notification, CancellationToken cancellationToken = default) + { + if (!await FeatureChecker.IsEnabledAsync(WeChatMiniProgramFeatures.Messages.Enable)) + { + Logger.LogWarning( + "{0} cannot push messages because the feature {0} is not enabled", + Name, + WeChatMiniProgramFeatures.Messages.Enable); + return false; + } + return true; + } + protected override async Task PublishAsync(NotificationInfo notification, IEnumerable identifiers, CancellationToken cancellationToken = default) { // step1 默认微信openid绑定的就是username, diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs index c55d763e9..15662db4e 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs @@ -30,12 +30,12 @@ public class IdentityWxPusherUserStore : IWxPusherUserStore { var user = await UserManager.FindByIdAsync(userId.ToString()); - var userUidClaim = user?.Claims - .Where(c => c.ClaimType.Equals(AbpWxPusherClaimTypes.Uid)) + var userTopicClaim = user?.Claims + .Where(c => c.ClaimType.Equals(AbpWxPusherClaimTypes.Topic)) .FirstOrDefault(); - if (userUidClaim != null && - int.TryParse(userUidClaim.ClaimValue, out var topic)) + if (userTopicClaim != null && + int.TryParse(userTopicClaim.ClaimValue, out var topic)) { topics.Add(topic); } @@ -43,4 +43,27 @@ public class IdentityWxPusherUserStore : IWxPusherUserStore return topics.Distinct().ToList(); } + + public async virtual Task> GetBindUidsAsync( + IEnumerable userIds, + CancellationToken cancellationToken = default) + { + var uids = new List(); + + foreach (var userId in userIds) + { + var user = await UserManager.FindByIdAsync(userId.ToString()); + + var userUidClaim = user?.Claims + .Where(c => c.ClaimType.Equals(AbpWxPusherClaimTypes.Uid)) + .FirstOrDefault(); + + if (userUidClaim != null) + { + uids.Add(userUidClaim.ClaimValue); + } + } + + return uids.Distinct().ToList(); + } } diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xml b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xml new file mode 100644 index 000000000..ac6b5b292 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xsd b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xsd new file mode 100644 index 000000000..11da52550 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs new file mode 100644 index 000000000..429e659a3 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDataExtensions.cs @@ -0,0 +1,19 @@ +namespace LINGYUN.Abp.Notifications; +public static class NotificationDataExtensions +{ + private const string Prefix = "wx-pusher:"; + private const string UrlKey = Prefix + "url"; + + public static void SetUrl( + this NotificationData notificationData, + string url) + { + notificationData.TrySetData(UrlKey, url); + } + + public static string GetUrlOrNull( + this NotificationData notificationData) + { + return notificationData.TryGetData(UrlKey)?.ToString(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs index 3889d7df5..fa11e4645 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs @@ -8,6 +8,7 @@ public static class NotificationDefinitionExtensions private const string Prefix = "wx-pusher:"; private const string ContentTypeKey = Prefix + "contentType"; private const string TopicKey = Prefix + "topic"; + private const string UrlKey = Prefix + "url"; /// /// 设定消息内容类型 /// @@ -71,4 +72,32 @@ public static class NotificationDefinitionExtensions return new List(); } + /// + /// 用户点击标题跳转页面 + /// + /// 群组编码 + /// + /// + /// + /// + public static NotificationDefinition WithUrl( + this NotificationDefinition notification, + string url) + { + return notification.WithProperty(UrlKey, url); + } + /// + /// 获取标题跳转页面 + /// + /// + public static string GetUrlOrNull( + this NotificationDefinition notification) + { + if (notification.Properties.TryGetValue(UrlKey, out var urlDefine)) + { + return urlDefine.ToString(); + } + + return null; + } } diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs index 794e15038..9e6f57366 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs @@ -6,7 +6,7 @@ namespace LINGYUN.Abp.Notifications.WxPusher; [DependsOn( typeof(AbpNotificationModule), typeof(AbpWxPusherModule))] -public class AbpNotificationsPushPlusModule : AbpModule +public class AbpNotificationsWxPusherModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs index a9bf27a0a..ee966ae20 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs @@ -1,13 +1,16 @@ using LINGYUN.Abp.RealTime.Localization; +using LINGYUN.Abp.WxPusher.Features; using LINGYUN.Abp.WxPusher.Messages; using LINGYUN.Abp.WxPusher.User; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.Features; using Volo.Abp.Localization; namespace LINGYUN.Abp.Notifications.WxPusher; @@ -18,6 +21,8 @@ public class WxPusherNotificationPublishProvider : NotificationPublishProvider public override string Name => ProviderName; + protected IFeatureChecker FeatureChecker { get; } + protected IWxPusherUserStore WxPusherUserStore { get; } protected IWxPusherMessageSender WxPusherMessageSender { get; } @@ -29,12 +34,14 @@ public class WxPusherNotificationPublishProvider : NotificationPublishProvider protected INotificationDefinitionManager NotificationDefinitionManager { get; } public WxPusherNotificationPublishProvider( + IFeatureChecker featureChecker, IWxPusherUserStore wxPusherUserStore, IWxPusherMessageSender wxPusherMessageSender, IStringLocalizerFactory localizerFactory, IOptions localizationOptions, INotificationDefinitionManager notificationDefinitionManager) { + FeatureChecker = featureChecker; WxPusherUserStore = wxPusherUserStore; WxPusherMessageSender = wxPusherMessageSender; LocalizerFactory = localizerFactory; @@ -42,17 +49,31 @@ public class WxPusherNotificationPublishProvider : NotificationPublishProvider NotificationDefinitionManager = notificationDefinitionManager; } + protected async override Task CanPublishAsync(NotificationInfo notification, CancellationToken cancellationToken = default) + { + if (!await FeatureChecker.IsEnabledAsync(WxPusherFeatureNames.Message.Enable)) + { + Logger.LogWarning( + "{0} cannot push messages because the feature {1} is not enabled", + Name, + WxPusherFeatureNames.Message.Enable); + return false; + } + return true; + } + protected async override Task PublishAsync( NotificationInfo notification, IEnumerable identifiers, CancellationToken cancellationToken = default) { - var topics = await WxPusherUserStore - .GetSubscribeTopicsAsync( - identifiers.Select(x => x.UserId), - cancellationToken); + var subscribeUserIds = identifiers.Select(x => x.UserId); + + var topics = await WxPusherUserStore.GetSubscribeTopicsAsync(subscribeUserIds, cancellationToken); + var uids = await WxPusherUserStore.GetBindUidsAsync(subscribeUserIds, cancellationToken); var notificationDefine = NotificationDefinitionManager.GetOrNull(notification.Name); + var url = notification.Data.GetUrlOrNull() ?? notificationDefine?.GetUrlOrNull(); var topicDefine = notificationDefine?.GetTopics(); if (topicDefine.Any()) { @@ -71,6 +92,8 @@ public class WxPusherNotificationPublishProvider : NotificationPublishProvider summary: title, contentType: contentType, topicIds: topics, + uids: uids, + url: url, cancellationToken: cancellationToken); } else @@ -88,6 +111,8 @@ public class WxPusherNotificationPublishProvider : NotificationPublishProvider summary: title, contentType: contentType, topicIds: topics, + uids: uids, + url: url, cancellationToken: cancellationToken); } } diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs index 27b13ce08..b560fbc02 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs @@ -14,7 +14,7 @@ public class WxPusherFeatureDefinitionProvider : FeatureDefinitionProvider displayName: L("Features:WxPusher")); group.AddFeature( name: WxPusherFeatureNames.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:WxPusherEnable"), description: L("Features:WxPusherEnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); @@ -26,7 +26,7 @@ public class WxPusherFeatureDefinitionProvider : FeatureDefinitionProvider message.CreateChild( name: WxPusherFeatureNames.Message.Enable, - defaultValue: "true", + defaultValue: "false", displayName: L("Features:MessageEnable"), description: L("Features:MessageEnableDesc"), valueType: new ToggleStringValueType(new BooleanValueValidator())); diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs index 73e178a38..dc41503c9 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs @@ -5,5 +5,9 @@ public static class AbpWxPusherClaimTypes /// /// 用户的唯一标识 /// - public static string Uid { get; set; } = "wx-pusher-uid"; + public static string Uid { get; set; } = "wxp-uid"; + /// + /// 用户订阅topic + /// + public static string Topic { get; set; } = "wxp-topic"; } diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs index 340828fc5..e57815d12 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs @@ -16,4 +16,13 @@ public interface IWxPusherUserStore Task> GetSubscribeTopicsAsync( IEnumerable userIds, CancellationToken cancellationToken = default); + /// + /// 获取绑定用户uid列表 + /// + /// + /// + /// + Task> GetBindUidsAsync( + IEnumerable userIds, + CancellationToken cancellationToken = default); } diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs index 80ec02cfe..3751d8bf7 100644 --- a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs @@ -19,4 +19,11 @@ public sealed class NullWxPusherUserStore : IWxPusherUserStore { return Task.FromResult(new List()); } + + public Task> GetBindUidsAsync( + IEnumerable userIds, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } } diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj index 369a4e39c..5bf413239 100644 --- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj @@ -55,6 +55,7 @@ + @@ -71,6 +72,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs index 9cb850ee1..b2847ccee 100644 --- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs @@ -42,6 +42,8 @@ using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; using Volo.Abp.TextTemplating.Scriban; +using LINGYUN.Abp.Notifications.PushPlus; +using LINGYUN.Abp.Notifications.WxPusher; namespace LY.MicroService.RealtimeMessage; @@ -76,6 +78,8 @@ namespace LY.MicroService.RealtimeMessage; typeof(AbpNotificationsSmsModule), typeof(AbpNotificationsEmailingModule), typeof(AbpNotificationsSignalRModule), + typeof(AbpNotificationsWxPusherModule), + typeof(AbpNotificationsPushPlusModule), typeof(AbpNotificationsWeChatMiniProgramModule), typeof(AbpNotificationsExceptionHandlingModule), typeof(AbpTextTemplatingScribanModule), diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/FakeNotificationSender.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/FakeNotificationSender.cs new file mode 100644 index 000000000..713e2522d --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/FakeNotificationSender.cs @@ -0,0 +1,205 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; +using Volo.Abp.TextTemplating; + +namespace LINGYUN.Abp.Notifications; + +[Dependency(ReplaceServices = true)] +public class FakeNotificationSender : INotificationSender, ITransientDependency +{ + public ILogger Logger { get; set; } + + protected AbpNotificationOptions Options { get; } + + protected IJsonSerializer JsonSerializer { get; } + + protected ITemplateRenderer TemplateRenderer { get; } + + protected INotificationStore NotificationStore { get; } + + protected IStringLocalizerFactory StringLocalizerFactory { get; } + + protected INotificationDefinitionManager NotificationDefinitionManager { get; } + + protected INotificationSubscriptionManager NotificationSubscriptionManager { get; } + + protected INotificationPublishProviderManager NotificationPublishProviderManager { get; } + + public FakeNotificationSender( + IJsonSerializer jsonSerializer, + ITemplateRenderer templateRenderer, + IStringLocalizerFactory stringLocalizerFactory, + IOptions options, + INotificationStore notificationStore, + INotificationDefinitionManager notificationDefinitionManager, + INotificationSubscriptionManager notificationSubscriptionManager, + INotificationPublishProviderManager notificationPublishProviderManager) + { + Options = options.Value; + JsonSerializer = jsonSerializer; + TemplateRenderer = templateRenderer; + StringLocalizerFactory = stringLocalizerFactory; + NotificationStore = notificationStore; + NotificationDefinitionManager = notificationDefinitionManager; + NotificationSubscriptionManager = notificationSubscriptionManager; + NotificationPublishProviderManager = notificationPublishProviderManager; + + Logger = NullLogger.Instance; + } + + public async virtual Task SendNofiterAsync( + string name, + NotificationData data, + IEnumerable users = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info) + { + var notification = NotificationDefinitionManager.GetOrNull(name); + if (notification == null) + { + return ""; + } + + var notificationInfo = new NotificationInfo + { + Name = notification.Name, + CreationTime = DateTime.Now, + Data = data, + Severity = severity, + Lifetime = notification.NotificationLifetime, + TenantId = tenantId, + Type = notification.NotificationType + }; + notificationInfo.SetId(DateTimeOffset.Now.ToUnixTimeMilliseconds()); + + notificationInfo.Data = NotificationDataConverter.Convert(notificationInfo.Data); + + Logger.LogDebug($"Persistent notification {notificationInfo.Name}"); + + await NotificationStore.InsertNotificationAsync(notificationInfo); + + var providers = Enumerable.Reverse(NotificationPublishProviderManager.Providers); + + if (notification.Providers.Any()) + { + providers = providers.Where(p => notification.Providers.Contains(p.Name)); + } + + await PublishFromProvidersAsync( + providers, + users ?? new List(), + notificationInfo); + + return notificationInfo.Id; + } + + public async virtual Task SendNofiterAsync( + string name, + NotificationTemplate template, + IEnumerable users = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info) + { + var notification = NotificationDefinitionManager.GetOrNull(name); + if (notification == null) + { + return ""; + } + + var notificationInfo = new NotificationInfo + { + Name = notification.Name, + TenantId = tenantId, + Severity = severity, + Type = notification.NotificationType, + CreationTime = DateTime.Now, + Lifetime = notification.NotificationLifetime, + }; + notificationInfo.SetId(DateTimeOffset.Now.ToUnixTimeMilliseconds()); + + var title = notification.DisplayName.Localize(StringLocalizerFactory); + + var message = await TemplateRenderer.RenderAsync( + templateName: name, + model: template.ExtraProperties); + + var notificationData = new NotificationData(); + notificationData.WriteStandardData( + title: title, + message: message, + createTime: notificationInfo.CreationTime, + formUser: "Fake User"); + notificationData.ExtraProperties.AddIfNotContains(template.ExtraProperties); + + notificationInfo.Data = notificationData; + + Logger.LogDebug($"Persistent notification {notificationInfo.Name}"); + + // 持久化通知 + await NotificationStore.InsertNotificationAsync(notificationInfo); + + var providers = Enumerable.Reverse(NotificationPublishProviderManager.Providers); + + // 过滤用户指定提供者 + if (notification.Providers.Any()) + { + providers = providers.Where(p => notification.Providers.Contains(p.Name)); + } + + await PublishFromProvidersAsync( + providers, + users ?? new List(), + notificationInfo); + + return notificationInfo.Id; + } + + /// + /// 指定提供者发布通知 + /// + /// 提供者列表 + /// 通知信息 + /// + protected async Task PublishFromProvidersAsync( + IEnumerable providers, + IEnumerable users, + NotificationInfo notificationInfo) + { + foreach (var provider in providers) + { + await PublishAsync(provider, notificationInfo, users); + } + } + /// + /// 发布通知 + /// + /// 通知发布者 + /// 通知信息 + /// 订阅用户列表 + /// + protected async Task PublishAsync( + INotificationPublishProvider provider, + NotificationInfo notificationInfo, + IEnumerable subscriptionUserIdentifiers) + { + Logger.LogDebug($"Sending notification with provider {provider.Name}"); + var notifacationDataMapping = Options.NotificationDataMappings + .GetMapItemOrDefault(provider.Name, notificationInfo.Name); + if (notifacationDataMapping != null) + { + notificationInfo.Data = notifacationDataMapping.MappingFunc(notificationInfo.Data); + } + // 发布 + await provider.PublishAsync(notificationInfo, subscriptionUserIdentifiers); + + Logger.LogDebug($"Send notification {notificationInfo.Name} with provider {provider.Name} was successful"); + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/NotificationsTestsNames.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/NotificationsTestsNames.cs index bbf8b951d..2516405e3 100644 --- a/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/NotificationsTestsNames.cs +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.Tests/LINGYUN/Abp/Notifications/NotificationsTestsNames.cs @@ -2,7 +2,7 @@ { public static class NotificationsTestsNames { - public const string GroupName = "Abp.Notifications"; + public const string GroupName = "Abp.NotificationTests"; public const string Test1 = GroupName + ".Test1"; diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN.Abp.Notifications.WxPusher.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN.Abp.Notifications.WxPusher.Tests.csproj new file mode 100644 index 000000000..c5a7e8e7c --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN.Abp.Notifications.WxPusher.Tests.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + + false + + + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestBase.cs new file mode 100644 index 000000000..2a12ce9dd --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestBase.cs @@ -0,0 +1,7 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.Notifications.WxPusher; + +public abstract class AbpNotificationsWxPusherTestBase : AbpTestsBase +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestModule.cs new file mode 100644 index 000000000..4e56c313d --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherTestModule.cs @@ -0,0 +1,12 @@ +using LINGYUN.Abp.WxPusher; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Notifications.WxPusher; + +[DependsOn( + typeof(AbpNotificationsWxPusherModule), + typeof(AbpWxPusherTestModule), + typeof(AbpNotificationsTestsModule))] +public class AbpNotificationsWxPusherTestModule : AbpModule +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationSenderTests.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationSenderTests.cs new file mode 100644 index 000000000..490acfa02 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationSenderTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.Notifications.WxPusher; + +public class NotificationSenderTests : AbpNotificationsWxPusherTestBase +{ + protected INotificationSender NotificationSender { get; } + + public NotificationSenderTests() + { + NotificationSender = GetRequiredService(); + } + + [Theory] + [InlineData( + "Title from the Xunit unit test", + "Text content from the Xunit unit test. \r\n Click the link at the top to redirect baidu site.")] + public async Task Send_Text_Test( + string title, + string message) + { + var notificationData = new NotificationData(); + notificationData.WriteStandardData( + title, + message, + DateTime.Now, + "xUnit Test"); + notificationData.SetUrl("https://www.baidu.com/"); + + await NotificationSender.SendNofiterAsync( + NotificationsTestsNames.Test1, + notificationData); + } + + [Theory] + [InlineData( + "Title from the Xunit unit test", + "Html content from the Xunit unit test.
Click to redirect baidu site.")] + public async Task Send_Html_Test( + string title, + string message) + { + var notificationData = new NotificationData(); + notificationData.WriteStandardData( + title, + message, + DateTime.Now, + "xUnit Test"); + notificationData.SetUrl("https://www.baidu.com/"); + + await NotificationSender.SendNofiterAsync( + NotificationsTestsNames.Test2, + notificationData); + } + + [Theory] + [InlineData( + "Title from the Xunit unit test", + "**Markdown content from the Xunit unit test.**
Click to redirect baidu site.")] + public async Task Send_Markdown_Test( + string title, + string message) + { + var notificationData = new NotificationData(); + notificationData.WriteStandardData( + title, + message, + DateTime.Now, + "xUnit Test"); + notificationData.SetUrl("https://www.baidu.com/"); + + await NotificationSender.SendNofiterAsync( + NotificationsTestsNames.Test3, + notificationData); + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationsWxPusherTestsDefinitionProvider.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationsWxPusherTestsDefinitionProvider.cs new file mode 100644 index 000000000..7915886fa --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/LINGYUN/Abp/Notifications/WxPusher/NotificationsWxPusherTestsDefinitionProvider.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.WxPusher.Messages; +using System.Collections.Generic; +using System.Linq; + +namespace LINGYUN.Abp.Notifications.WxPusher; + +public class NotificationsWxPusherTestsDefinitionProvider : NotificationDefinitionProvider +{ + public override void Define(INotificationDefinitionContext context) + { + var group = context.GetGroupOrNull(NotificationsTestsNames.GroupName); + + var nt1 = group.Notifications.FirstOrDefault(n => n.Name.Equals(NotificationsTestsNames.Test1)); + nt1.WithProviders(WxPusherNotificationPublishProvider.ProviderName) + .WithContentType(MessageContentType.Text) + .WithTopics(new List { 7182 }); + + var nt2 = group.Notifications.FirstOrDefault(n => n.Name.Equals(NotificationsTestsNames.Test2)); + nt2.WithProviders(WxPusherNotificationPublishProvider.ProviderName) + .WithContentType(MessageContentType.Html) + .WithTopics(new List { 7182 }); + + var nt3 = group.Notifications.FirstOrDefault(n => n.Name.Equals(NotificationsTestsNames.Test3)); + nt3.WithProviders(WxPusherNotificationPublishProvider.ProviderName) + .WithContentType(MessageContentType.Markdown) + .WithTopics(new List { 7182 }); + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/Usings.cs b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/Usings.cs new file mode 100644 index 000000000..5cf9e838b --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.Notifications.WxPusher.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using Shouldly; \ No newline at end of file