diff --git a/README.md b/README.md index 58a5e6e..a872de3 100644 --- a/README.md +++ b/README.md @@ -45,31 +45,32 @@ subscription pushMessageCreated { ```js { pushMessages (unreadOnly: true, cultureName: "en-Us") { - unreadCount + totalCount items { id shortMessage createdDate isRead + isHidden } } } ``` ### Mutations ```js -mutation clearAllPushMessages{ +mutation clearAllPushMessages { clearAllPushMessages } ``` ```js -mutation markAllPushMessagesRead{ +mutation markAllPushMessagesRead { markAllPushMessagesRead } ``` ```js -mutation markAllPushMessagesUnread{ +mutation markAllPushMessagesUnread { markAllPushMessagesUnread } ``` diff --git a/VirtoCommerce.PushMessages.sln.DotSettings b/VirtoCommerce.PushMessages.sln.DotSettings index 8b21242..a2ea974 100644 --- a/VirtoCommerce.PushMessages.sln.DotSettings +++ b/VirtoCommerce.PushMessages.sln.DotSettings @@ -1,4 +1,4 @@ - + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> @@ -8,4 +8,5 @@ True True True + True diff --git a/src/VirtoCommerce.PushMessages.Core/Events/FcmTokenChangedEvent.cs b/src/VirtoCommerce.PushMessages.Core/Events/FcmTokenChangedEvent.cs new file mode 100644 index 0000000..636c362 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Events/FcmTokenChangedEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Core.Events; + +public class FcmTokenChangedEvent : GenericChangedEntryEvent +{ + public FcmTokenChangedEvent(IEnumerable> changedEntries) + : base(changedEntries) + { + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Events/FcmTokenChangingEvent.cs b/src/VirtoCommerce.PushMessages.Core/Events/FcmTokenChangingEvent.cs new file mode 100644 index 0000000..9f3282e --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Events/FcmTokenChangingEvent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Core.Events; + +public class FcmTokenChangingEvent : GenericChangedEntryEvent +{ + public FcmTokenChangingEvent(IEnumerable> changedEntries) + : base(changedEntries) + { + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/EnumerableExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..605a43c --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Extensions/EnumerableExtensions.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Linq; + +namespace VirtoCommerce.PushMessages.Core.Extensions; + +public static class EnumerableExtensions +{ + public static IList ToIList(this IEnumerable enumerable) + { + return enumerable.ToList(); + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageRecipientChangedEventExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageRecipientChangedEventExtensions.cs new file mode 100644 index 0000000..a6fb664 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageRecipientChangedEventExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Core.Extensions; + +public static class PushMessageRecipientChangedEventExtensions +{ + public static IDictionary> GetMessageIdsAndRecipients(this PushMessageRecipientChangedEvent @event) + { + return @event.ChangedEntries + .Where(x => x.EntryState == EntryState.Added) + .GroupBy(x => x.NewEntry.MessageId) + .ToDictionary(g => g.Key, g => g.Select(x => x.NewEntry).ToIList()); + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/FcmReceiverOptions.cs b/src/VirtoCommerce.PushMessages.Core/Models/FcmReceiverOptions.cs new file mode 100644 index 0000000..d5ac24f --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/FcmReceiverOptions.cs @@ -0,0 +1,12 @@ +namespace VirtoCommerce.PushMessages.Core.Models; + +public class FcmReceiverOptions +{ + public string ApiKey { get; set; } + public string AuthDomain { get; set; } + public string ProjectId { get; set; } + public string StorageBucket { get; set; } + public string MessagingSenderId { get; set; } + public string AppId { get; set; } + public string VapidKey { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/FcmSenderOptions.cs b/src/VirtoCommerce.PushMessages.Core/Models/FcmSenderOptions.cs new file mode 100644 index 0000000..e72dbac --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/FcmSenderOptions.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; + +namespace VirtoCommerce.PushMessages.Core.Models; + +public class FcmSenderOptions +{ + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("project_id")] + public string ProjectId { get; set; } + + [JsonProperty("private_key_id")] + public string PrivateKeyId { get; set; } + + [JsonProperty("private_key")] + public string PrivateKey { get; set; } + + [JsonProperty("client_email")] + public string ClientEmail { get; set; } + + [JsonProperty("client_id")] + public string ClientId { get; set; } + + [JsonProperty("auth_uri")] + public string AuthUri { get; set; } + + [JsonProperty("token_uri")] + public string TokenUri { get; set; } + + [JsonProperty("auth_provider_x509_cert_url")] + public string AuthProviderX509CertUrl { get; set; } + + [JsonProperty("client_x509_cert_url")] + public string ClientX509CertUrl { get; set; } + + [JsonProperty("universe_domain")] + public string UniverseDomain { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/FcmToken.cs b/src/VirtoCommerce.PushMessages.Core/Models/FcmToken.cs new file mode 100644 index 0000000..c54d33a --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/FcmToken.cs @@ -0,0 +1,16 @@ +using System; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.PushMessages.Core.Models; + +public class FcmToken : AuditableEntity, ICloneable +{ + public string Token { get; set; } + + public string UserId { get; set; } + + public object Clone() + { + return MemberwiseClone(); + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/FcmTokenSearchCriteria.cs b/src/VirtoCommerce.PushMessages.Core/Models/FcmTokenSearchCriteria.cs new file mode 100644 index 0000000..b7e27b4 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/FcmTokenSearchCriteria.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.PushMessages.Core.Models; + +public class FcmTokenSearchCriteria : SearchCriteriaBase +{ + public string Token { get; set; } + + public IList UserIds { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/FcmTokenSearchResult.cs b/src/VirtoCommerce.PushMessages.Core/Models/FcmTokenSearchResult.cs new file mode 100644 index 0000000..090c61f --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/FcmTokenSearchResult.cs @@ -0,0 +1,7 @@ +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.PushMessages.Core.Models; + +public class FcmTokenSearchResult : GenericSearchResult +{ +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageOptions.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageOptions.cs new file mode 100644 index 0000000..c737fc5 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageOptions.cs @@ -0,0 +1,8 @@ +namespace VirtoCommerce.PushMessages.Core.Models; + +public class PushMessageOptions +{ + public bool UseFirebaseCloudMessaging { get; set; } + public FcmSenderOptions FcmSenderOptions { get; set; } + public FcmReceiverOptions FcmReceiverOptions { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Services/IFcmTokenSearchService.cs b/src/VirtoCommerce.PushMessages.Core/Services/IFcmTokenSearchService.cs new file mode 100644 index 0000000..01b0d7c --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Services/IFcmTokenSearchService.cs @@ -0,0 +1,8 @@ +using VirtoCommerce.Platform.Core.GenericCrud; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Core.Services; + +public interface IFcmTokenSearchService : ISearchService +{ +} diff --git a/src/VirtoCommerce.PushMessages.Core/Services/IFcmTokenService.cs b/src/VirtoCommerce.PushMessages.Core/Services/IFcmTokenService.cs new file mode 100644 index 0000000..116e32f --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Services/IFcmTokenService.cs @@ -0,0 +1,8 @@ +using VirtoCommerce.Platform.Core.GenericCrud; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Core.Services; + +public interface IFcmTokenService : ICrudService +{ +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240604144008_AddFcmToken.Designer.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240604144008_AddFcmToken.Designer.cs new file mode 100644 index 0000000..e996b96 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240604144008_AddFcmToken.Designer.cs @@ -0,0 +1,242 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.MySql.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240604144008_AddFcmToken")] + partial class AddFcmToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.FcmTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + + b.HasIndex("Token", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId"); + + b.ToTable("PushMessageFcmToken", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("TrackNewRecipients") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("IsHidden") + .HasColumnType("tinyint(1)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240604144008_AddFcmToken.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240604144008_AddFcmToken.cs new file mode 100644 index 0000000..85a9374 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240604144008_AddFcmToken.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.MySql.Migrations +{ + /// + public partial class AddFcmToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PushMessageFcmToken", + columns: table => new + { + Id = table.Column(type: "varchar(128)", maxLength: 128, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Token = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + UserId = table.Column(type: "varchar(128)", maxLength: 128, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedDate = table.Column(type: "datetime(6)", nullable: false), + ModifiedDate = table.Column(type: "datetime(6)", nullable: true), + CreatedBy = table.Column(type: "varchar(64)", maxLength: 64, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ModifiedBy = table.Column(type: "varchar(64)", maxLength: 64, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_PushMessageFcmToken", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_PushMessageFcmToken_Token_UserId", + table: "PushMessageFcmToken", + columns: new[] { "Token", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PushMessageFcmToken_UserId", + table: "PushMessageFcmToken", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PushMessageFcmToken"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs index 9e7d86e..4f9eead 100644 --- a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -2,6 +2,7 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using VirtoCommerce.PushMessages.Data.Repositories; @@ -16,9 +17,52 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 64); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.FcmTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + + b.HasIndex("Token", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId"); + + b.ToTable("PushMessageFcmToken", (string)null); + }); + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => { b.Property("Id") diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240604144016_AddFcmToken.Designer.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240604144016_AddFcmToken.Designer.cs new file mode 100644 index 0000000..5cb4354 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240604144016_AddFcmToken.Designer.cs @@ -0,0 +1,242 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.PostgreSql.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240604144016_AddFcmToken")] + partial class AddFcmToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.FcmTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + + b.HasIndex("Token", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId"); + + b.ToTable("PushMessageFcmToken", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TrackNewRecipients") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240604144016_AddFcmToken.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240604144016_AddFcmToken.cs new file mode 100644 index 0000000..95245c5 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240604144016_AddFcmToken.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.PostgreSql.Migrations +{ + /// + public partial class AddFcmToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PushMessageFcmToken", + columns: table => new + { + Id = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + Token = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + UserId = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), + ModifiedDate = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + ModifiedBy = table.Column(type: "character varying(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PushMessageFcmToken", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_PushMessageFcmToken_Token_UserId", + table: "PushMessageFcmToken", + columns: new[] { "Token", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PushMessageFcmToken_UserId", + table: "PushMessageFcmToken", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PushMessageFcmToken"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs index 63c4dd2..e43009d 100644 --- a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -17,11 +17,52 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.FcmTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + + b.HasIndex("Token", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId"); + + b.ToTable("PushMessageFcmToken", (string)null); + }); + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => { b.Property("Id") diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240604142524_AddFcmToken.Designer.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240604142524_AddFcmToken.Designer.cs new file mode 100644 index 0000000..0cadc76 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240604142524_AddFcmToken.Designer.cs @@ -0,0 +1,245 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.SqlServer.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240604142524_AddFcmToken")] + partial class AddFcmToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.FcmTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("Token") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + + b.HasIndex("Token", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId") + .HasFilter("[Token] IS NOT NULL AND [UserId] IS NOT NULL"); + + b.ToTable("PushMessageFcmToken", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("MemberQuery") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TrackNewRecipients") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId") + .HasFilter("[MemberId] IS NOT NULL"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("IsHidden") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId") + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240604142524_AddFcmToken.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240604142524_AddFcmToken.cs new file mode 100644 index 0000000..85ca986 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240604142524_AddFcmToken.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.SqlServer.Migrations +{ + /// + public partial class AddFcmToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PushMessageFcmToken", + columns: table => new + { + Id = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + Token = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + UserId = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + CreatedDate = table.Column(type: "datetime2", nullable: false), + ModifiedDate = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + ModifiedBy = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PushMessageFcmToken", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_PushMessageFcmToken_Token_UserId", + table: "PushMessageFcmToken", + columns: new[] { "Token", "UserId" }, + unique: true, + filter: "[Token] IS NOT NULL AND [UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_PushMessageFcmToken_UserId", + table: "PushMessageFcmToken", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PushMessageFcmToken"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs index c1d7e3b..af3ab96 100644 --- a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -22,6 +22,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.FcmTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("Token") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + + b.HasIndex("Token", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId") + .HasFilter("[Token] IS NOT NULL AND [UserId] IS NOT NULL"); + + b.ToTable("PushMessageFcmToken", (string)null); + }); + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => { b.Property("Id") diff --git a/src/VirtoCommerce.PushMessages.Data/Extensions/ApplicationBuilderExtensions.cs b/src/VirtoCommerce.PushMessages.Data/Extensions/ApplicationBuilderExtensions.cs index 1769734..49a5cb9 100644 --- a/src/VirtoCommerce.PushMessages.Data/Extensions/ApplicationBuilderExtensions.cs +++ b/src/VirtoCommerce.PushMessages.Data/Extensions/ApplicationBuilderExtensions.cs @@ -1,8 +1,19 @@ +using System.Linq; +using System.Reflection; +using FirebaseAdmin; +using Google.Apis.Auth.OAuth2; using Hangfire; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.Platform.Core.Settings; using VirtoCommerce.Platform.Hangfire; +using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Data.BackgroundJobs; +using VirtoCommerce.PushMessages.Data.Handlers; using JobSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.BackgroundJobs; namespace VirtoCommerce.PushMessages.Data.Extensions; @@ -29,4 +40,46 @@ public static IApplicationBuilder UsePushMessageJobs(this IApplicationBuilder ap return appBuilder; } + + public static void UseFirebaseCloudMessaging(this IApplicationBuilder appBuilder, string moduleId) + { + var options = appBuilder.ApplicationServices.GetService>().Value; + + if (!options.UseFirebaseCloudMessaging) + { + return; + } + + var json = JsonConvert.SerializeObject(options.FcmSenderOptions); + var appOptions = new AppOptions { Credential = GoogleCredential.FromJson(json) }; + FirebaseApp.Create(appOptions); + + appBuilder.RegisterEventHandler(); + + var receiverSettings = options.FcmReceiverOptions.ToSettings(); + var settingsRegistrar = appBuilder.ApplicationServices.GetRequiredService(); + settingsRegistrar.RegisterSettings(receiverSettings, moduleId); + settingsRegistrar.RegisterSettingsForType(receiverSettings, "Store"); + } + + private static SettingDescriptor[] ToSettings(this FcmReceiverOptions options) + { + return options + .GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Select(x => CreateSetting(x.Name, x.GetValue(options))) + .ToArray(); + } + + private static SettingDescriptor CreateSetting(string name, object value) + { + return new SettingDescriptor + { + Name = $"PushMessages.{nameof(FcmReceiverOptions)}.{name}", + GroupName = "Push Messages|FCM Receiver Options", + ValueType = SettingValueType.ShortText, + DefaultValue = value, + IsPublic = true, + }; + } } diff --git a/src/VirtoCommerce.PushMessages.Data/Handlers/FcmPushMessageRecipientChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.Data/Handlers/FcmPushMessageRecipientChangedEventHandler.cs new file mode 100644 index 0000000..5929f8c --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Handlers/FcmPushMessageRecipientChangedEventHandler.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FirebaseAdmin.Messaging; +using Microsoft.Extensions.Logging; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Extensions; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; +using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; + +namespace VirtoCommerce.PushMessages.Data.Handlers; + +public class FcmPushMessageRecipientChangedEventHandler : IEventHandler +{ + private readonly IPushMessageService _pushMessageService; + private readonly IFcmTokenSearchService _fcmTokenSearchService; + private readonly ISettingsManager _settingsManager; + private readonly ILogger _logger; + + public FcmPushMessageRecipientChangedEventHandler( + IPushMessageService pushMessageService, + IFcmTokenSearchService fcmTokenSearchService, + ISettingsManager settingsManager, + ILogger logger) + { + _pushMessageService = pushMessageService; + _fcmTokenSearchService = fcmTokenSearchService; + _settingsManager = settingsManager; + _logger = logger; + } + + public async Task Handle(PushMessageRecipientChangedEvent message) + { + foreach (var (messageId, recipients) in message.GetMessageIdsAndRecipients()) + { + var pushMessage = await _pushMessageService.GetNoCloneAsync(messageId); + await SendMessageAsync(pushMessage, recipients); + } + } + + public async Task SendMessageAsync(PushMessage message, IList recipients) + { + var firebaseMessage = new MulticastMessage + { + Data = new Dictionary + { + { "messageId", message.Id }, + { "body", message.ShortMessage }, + }, + }; + + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.UserIds = recipients.Select(x => x.UserId).ToList(); + searchCriteria.Take = await GetBatchSize(); + + await foreach (var searchResult in _fcmTokenSearchService.SearchBatchesNoCloneAsync(searchCriteria)) + { + firebaseMessage.Tokens = searchResult.Results.Select(x => x.Token).ToArray(); + await SendFirebaseMessage(firebaseMessage); + } + } + + private async Task SendFirebaseMessage(MulticastMessage firebaseMessage) + { + try + { + var batchResponse = await FirebaseMessaging.DefaultInstance.SendEachForMulticastAsync(firebaseMessage); + + if (batchResponse.FailureCount == 0) + { + return; + } + + foreach (var response in batchResponse.Responses.Where(x => !x.IsSuccess)) + { + _logger.LogError("FCM Send failed: {Exception}", response.Exception); + } + } + catch (Exception ex) + { + _logger.LogError("FCM Send failed. {Exception}", ex); + } + } + + private Task GetBatchSize() + { + return _settingsManager.GetValueAsync(GeneralSettings.BatchSize); + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Models/FcmTokenEntity.cs b/src/VirtoCommerce.PushMessages.Data/Models/FcmTokenEntity.cs new file mode 100644 index 0000000..3673819 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Models/FcmTokenEntity.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Domain; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Data.Models; + +public class FcmTokenEntity : AuditableEntity, IDataEntity +{ + [StringLength(256)] + public string Token { get; set; } + + [StringLength(128)] + public string UserId { get; set; } + + public virtual FcmToken ToModel(FcmToken model) + { + model.Id = Id; + model.CreatedBy = CreatedBy; + model.CreatedDate = CreatedDate; + model.ModifiedBy = ModifiedBy; + model.ModifiedDate = ModifiedDate; + + model.Token = Token; + model.UserId = UserId; + + return model; + } + + public virtual FcmTokenEntity FromModel(FcmToken model, PrimaryKeyResolvingMap pkMap) + { + pkMap.AddPair(model, this); + + Id = model.Id; + CreatedBy = model.CreatedBy; + CreatedDate = model.CreatedDate; + ModifiedBy = model.ModifiedBy; + ModifiedDate = model.ModifiedDate; + + Token = model.Token; + UserId = model.UserId; + + return this; + } + + public virtual void Patch(FcmTokenEntity target) + { + target.Token = Token; + target.UserId = UserId; + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Repositories/IPushMessagesRepository.cs b/src/VirtoCommerce.PushMessages.Data/Repositories/IPushMessagesRepository.cs index 3adf693..2e31af9 100644 --- a/src/VirtoCommerce.PushMessages.Data/Repositories/IPushMessagesRepository.cs +++ b/src/VirtoCommerce.PushMessages.Data/Repositories/IPushMessagesRepository.cs @@ -14,7 +14,11 @@ public interface IPushMessagesRepository : IRepository public IQueryable Recipients { get; } + public IQueryable FcmTokens { get; } + Task> GetMessagesByIdsAsync(IList ids, string responseGroup); Task> GetRecipientsByIdsAsync(IList ids, string responseGroup); + + Task> GetFcmTokensByIdsAsync(IList ids, string responseGroup); } diff --git a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs index a58837a..0643adb 100644 --- a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs +++ b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesDbContext.cs @@ -7,6 +7,8 @@ namespace VirtoCommerce.PushMessages.Data.Repositories; public class PushMessagesDbContext : DbContextBase { + private const int _idLength = 128; + public PushMessagesDbContext(DbContextOptions options) : base(options) { @@ -22,10 +24,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); modelBuilder.Entity().ToTable("PushMessage").HasKey(x => x.Id); - modelBuilder.Entity().Property(x => x.Id).HasMaxLength(128).ValueGeneratedOnAdd(); + modelBuilder.Entity().Property(x => x.Id).HasMaxLength(_idLength).ValueGeneratedOnAdd(); modelBuilder.Entity().ToTable("PushMessageMember").HasKey(x => x.Id); - modelBuilder.Entity().Property(x => x.Id).HasMaxLength(128).ValueGeneratedOnAdd(); + modelBuilder.Entity().Property(x => x.Id).HasMaxLength(_idLength).ValueGeneratedOnAdd(); modelBuilder.Entity().HasOne(x => x.Message).WithMany(x => x.Members) .HasForeignKey(x => x.MessageId).OnDelete(DeleteBehavior.Cascade).IsRequired(); modelBuilder.Entity() @@ -34,7 +36,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); modelBuilder.Entity().ToTable("PushMessageRecipient").HasKey(x => x.Id); - modelBuilder.Entity().Property(x => x.Id).HasMaxLength(128).ValueGeneratedOnAdd(); + modelBuilder.Entity().Property(x => x.Id).HasMaxLength(_idLength).ValueGeneratedOnAdd(); modelBuilder.Entity().HasOne(x => x.Message).WithMany() .HasForeignKey(x => x.MessageId).OnDelete(DeleteBehavior.Cascade).IsRequired(); modelBuilder.Entity() @@ -42,6 +44,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsUnique() .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + modelBuilder.Entity().ToTable("PushMessageFcmToken").HasKey(x => x.Id); + modelBuilder.Entity().Property(x => x.Id).HasMaxLength(_idLength).ValueGeneratedOnAdd(); + modelBuilder.Entity() + .HasIndex(x => x.UserId) + .HasDatabaseName("IX_PushMessageFcmToken_UserId"); + modelBuilder.Entity() + .HasIndex(x => new { x.Token, x.UserId }) + .IsUnique() + .HasDatabaseName("IX_PushMessageFcmToken_Token_UserId"); + switch (Database.ProviderName) { case "Pomelo.EntityFrameworkCore.MySql": diff --git a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs index 8389c84..4dd6699 100644 --- a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs +++ b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs @@ -23,6 +23,8 @@ public PushMessagesRepository(PushMessagesDbContext dbContext, IUnitOfWork unitO public IQueryable Recipients => DbContext.Set(); + public IQueryable FcmTokens => DbContext.Set(); + public virtual async Task> GetMessagesByIdsAsync(IList ids, string responseGroup) { if (ids.IsNullOrEmpty()) @@ -72,4 +74,16 @@ public virtual async Task> GetRecipientsByIdsA return recipients; } + + public virtual async Task> GetFcmTokensByIdsAsync(IList ids, string responseGroup) + { + if (ids.IsNullOrEmpty()) + { + return []; + } + + return ids.Count == 1 + ? await FcmTokens.Where(x => x.Id == ids.First()).ToListAsync() + : await FcmTokens.Where(x => ids.Contains(x.Id)).ToListAsync(); + } } diff --git a/src/VirtoCommerce.PushMessages.Data/Services/FcmTokenSearchService.cs b/src/VirtoCommerce.PushMessages.Data/Services/FcmTokenSearchService.cs new file mode 100644 index 0000000..70061d8 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Services/FcmTokenSearchService.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; +using VirtoCommerce.Platform.Core.Caching; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.GenericCrud; +using VirtoCommerce.Platform.Data.GenericCrud; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; +using VirtoCommerce.PushMessages.Data.Models; +using VirtoCommerce.PushMessages.Data.Repositories; + +namespace VirtoCommerce.PushMessages.Data.Services; + +public class FcmTokenSearchService : SearchService, IFcmTokenSearchService +{ + public FcmTokenSearchService( + Func repositoryFactory, + IPlatformMemoryCache platformMemoryCache, + IFcmTokenService crudService, + IOptions crudOptions) + : base(repositoryFactory, platformMemoryCache, crudService, crudOptions) + { + } + + protected override IQueryable BuildQuery(IRepository repository, FcmTokenSearchCriteria criteria) + { + var query = ((IPushMessagesRepository)repository).FcmTokens; + + if (criteria.Token != null) + { + query = query.Where(x => x.Token == criteria.Token); + } + + if (!criteria.UserIds.IsNullOrEmpty()) + { + query = criteria.UserIds.Count == 1 + ? query.Where(x => x.UserId == criteria.UserIds.First()) + : query.Where(x => criteria.UserIds.Contains(x.UserId)); + } + + return query; + } + + protected override IList BuildSortExpression(FcmTokenSearchCriteria criteria) + { + var sortInfos = criteria.SortInfos; + + if (sortInfos.IsNullOrEmpty()) + { + sortInfos = + [ + new SortInfo { SortColumn = nameof(FcmTokenEntity.CreatedDate), SortDirection = SortDirection.Descending }, + new SortInfo { SortColumn = nameof(FcmTokenEntity.Id) }, + ]; + } + + return sortInfos; + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Services/FcmTokenService.cs b/src/VirtoCommerce.PushMessages.Data/Services/FcmTokenService.cs new file mode 100644 index 0000000..ab6a3d5 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Services/FcmTokenService.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Caching; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.Platform.Data.GenericCrud; +using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; +using VirtoCommerce.PushMessages.Data.Models; +using VirtoCommerce.PushMessages.Data.Repositories; + +namespace VirtoCommerce.PushMessages.Data.Services; + +public class FcmTokenService : CrudService, IFcmTokenService +{ + public FcmTokenService( + Func repositoryFactory, + IPlatformMemoryCache platformMemoryCache, + IEventPublisher eventPublisher) + : base(repositoryFactory, platformMemoryCache, eventPublisher) + { + } + + protected override Task> LoadEntities(IRepository repository, IList ids, string responseGroup) + { + return ((IPushMessagesRepository)repository).GetFcmTokensByIdsAsync(ids, responseGroup); + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs index 1034253..99e4505 100644 --- a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs +++ b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs @@ -123,7 +123,7 @@ private async Task CalculateReadRate(IList messages) private static int CalculatePercent(int readCount, int totalCount) { - if (totalCount <= 0) + if (readCount == 0 || totalCount == 0) { return 0; } diff --git a/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj b/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj index 9b6d8ee..482cdad 100644 --- a/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj +++ b/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj @@ -7,6 +7,7 @@ false + diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommand.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommand.cs new file mode 100644 index 0000000..55e2b29 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommand.cs @@ -0,0 +1,10 @@ +using VirtoCommerce.ExperienceApiModule.Core.Infrastructure; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Commands; + +public class AddFcmTokenCommand : ICommand +{ + public string Token { get; set; } + + public string UserId { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommandBuilder.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommandBuilder.cs new file mode 100644 index 0000000..f028d02 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommandBuilder.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Types; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using VirtoCommerce.ExperienceApiModule.Core.BaseQueries; +using VirtoCommerce.ExperienceApiModule.Core.Extensions; +using VirtoCommerce.PushMessages.ExperienceApi.Authorization; +using VirtoCommerce.PushMessages.ExperienceApi.Schemas; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Commands; + +public class AddFcmTokenCommandBuilder : CommandBuilder +{ + protected override string Name => "addFcmToken"; + + public AddFcmTokenCommandBuilder(IMediator mediator, IAuthorizationService authorizationService) + : base(mediator, authorizationService) + { + } + + protected override async Task BeforeMediatorSend(IResolveFieldContext context, AddFcmTokenCommand request) + { + await Authorize(context, null, new PushMessagesAuthorizationRequirement()); + + request.UserId = context.GetCurrentUserId(); + + await base.BeforeMediatorSend(context, request); + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommandHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommandHandler.cs new file mode 100644 index 0000000..30fc284 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/AddFcmTokenCommandHandler.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Commands; + +public class AddFcmTokenCommandHandler : IRequestHandler +{ + private readonly IFcmTokenService _fcmTokenService; + private readonly IFcmTokenSearchService _fcmTokenSearchService; + + public AddFcmTokenCommandHandler( + IFcmTokenService fcmTokenService, + IFcmTokenSearchService fcmTokenSearchService) + { + _fcmTokenService = fcmTokenService; + _fcmTokenSearchService = fcmTokenSearchService; + } + + public async Task Handle(AddFcmTokenCommand request, CancellationToken cancellationToken) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.Token = request.Token; + searchCriteria.UserIds = [request.UserId]; + + var searchResult = await _fcmTokenSearchService.SearchAsync(searchCriteria); + var fcmToken = searchResult.Results.FirstOrDefault(); + + if (fcmToken is null) + { + fcmToken = AbstractTypeFactory.TryCreateInstance(); + fcmToken.Token = request.Token; + fcmToken.UserId = request.UserId; + } + + await _fcmTokenService.SaveChangesAsync([fcmToken]); + + return true; + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommand.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommand.cs new file mode 100644 index 0000000..e65936b --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommand.cs @@ -0,0 +1,10 @@ +using VirtoCommerce.ExperienceApiModule.Core.Infrastructure; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Commands; + +public class DeleteFcmTokenCommand : ICommand +{ + public string Token { get; set; } + + public string UserId { get; set; } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommandBuilder.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommandBuilder.cs new file mode 100644 index 0000000..7a705bb --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommandBuilder.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Types; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using VirtoCommerce.ExperienceApiModule.Core.BaseQueries; +using VirtoCommerce.ExperienceApiModule.Core.Extensions; +using VirtoCommerce.PushMessages.ExperienceApi.Authorization; +using VirtoCommerce.PushMessages.ExperienceApi.Schemas; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Commands; + +public class DeleteFcmTokenCommandBuilder : CommandBuilder +{ + protected override string Name => "deleteFcmToken"; + + public DeleteFcmTokenCommandBuilder(IMediator mediator, IAuthorizationService authorizationService) + : base(mediator, authorizationService) + { + } + + protected override async Task BeforeMediatorSend(IResolveFieldContext context, DeleteFcmTokenCommand request) + { + await Authorize(context, null, new PushMessagesAuthorizationRequirement()); + + request.UserId = context.GetCurrentUserId(); + + await base.BeforeMediatorSend(context, request); + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommandHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommandHandler.cs new file mode 100644 index 0000000..703f061 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/DeleteFcmTokenCommandHandler.cs @@ -0,0 +1,39 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.PushMessages.Core.Extensions; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Commands; + +public class DeleteFcmTokenCommandHandler : IRequestHandler +{ + private readonly IFcmTokenService _fcmTokenService; + private readonly IFcmTokenSearchService _fcmTokenSearchService; + + public DeleteFcmTokenCommandHandler( + IFcmTokenService fcmTokenService, + IFcmTokenSearchService fcmTokenSearchService) + { + _fcmTokenService = fcmTokenService; + _fcmTokenSearchService = fcmTokenSearchService; + } + + public async Task Handle(DeleteFcmTokenCommand request, CancellationToken cancellationToken) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.Token = request.Token; + searchCriteria.UserIds = [request.UserId]; + + await _fcmTokenSearchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => + { + var ids = searchResult.Results.Select(x => x.Id).ToList(); + await _fcmTokenService.DeleteAsync(ids); + }); + + return true; + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageRecipientChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/XapiPushMessageRecipientChangedEventHandler.cs similarity index 71% rename from src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageRecipientChangedEventHandler.cs rename to src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/XapiPushMessageRecipientChangedEventHandler.cs index ba6c26e..5594e58 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageRecipientChangedEventHandler.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/XapiPushMessageRecipientChangedEventHandler.cs @@ -1,20 +1,20 @@ -using System.Linq; using System.Threading.Tasks; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Services; using VirtoCommerce.PushMessages.ExperienceApi.Models; using VirtoCommerce.PushMessages.ExperienceApi.Subscriptions; namespace VirtoCommerce.PushMessages.ExperienceApi.Handlers; -public class PushMessageRecipientChangedEventHandler : IEventHandler +public class XapiPushMessageRecipientChangedEventHandler : IEventHandler { private readonly IPushMessageService _pushMessageService; private readonly IPushMessageHub _eventBroker; - public PushMessageRecipientChangedEventHandler( + public XapiPushMessageRecipientChangedEventHandler( IPushMessageService pushMessageService, IPushMessageHub eventBroker) { @@ -24,10 +24,7 @@ public PushMessageRecipientChangedEventHandler( public async Task Handle(PushMessageRecipientChangedEvent message) { - foreach (var (messageId, recipients) in message.ChangedEntries - .Where(x => x.EntryState == EntryState.Added) - .GroupBy(x => x.NewEntry.MessageId) - .ToDictionary(g => g.Key, g => g.Select(x => x.NewEntry))) + foreach (var (messageId, recipients) in message.GetMessageIdsAndRecipients()) { var pushMessage = await _pushMessageService.GetNoCloneAsync(messageId); diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQuery.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQuery.cs new file mode 100644 index 0000000..1847993 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQuery.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; +using VirtoCommerce.ExperienceApiModule.Core.BaseQueries; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Queries; + +public class GetFcmSettingsQuery : Query +{ + public string UserId { get; set; } + + public override IEnumerable GetArguments() + { + yield break; + } + + public override void Map(IResolveFieldContext context) + { + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQueryBuilder.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQueryBuilder.cs new file mode 100644 index 0000000..9e0f58a --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQueryBuilder.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using GraphQL; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using VirtoCommerce.ExperienceApiModule.Core.BaseQueries; +using VirtoCommerce.ExperienceApiModule.Core.Extensions; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.ExperienceApi.Authorization; +using VirtoCommerce.PushMessages.ExperienceApi.Schemas; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Queries; + +public class GetFcmSettingsQueryBuilder : QueryBuilder +{ + protected override string Name => "fcmSettings"; + + public GetFcmSettingsQueryBuilder(IMediator mediator, IAuthorizationService authorizationService) + : base(mediator, authorizationService) + { + } + + protected override async Task BeforeMediatorSend(IResolveFieldContext context, GetFcmSettingsQuery request) + { + await Authorize(context, null, new PushMessagesAuthorizationRequirement()); + context.CopyArgumentsToUserContext(); + request.UserId = context.GetCurrentUserId(); + + await base.BeforeMediatorSend(context, request); + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQueryHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQueryHandler.cs new file mode 100644 index 0000000..7684a65 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Queries/GetFcmSettingsQueryHandler.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using VirtoCommerce.ExperienceApiModule.Core.Infrastructure; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Queries; + +public class GetFcmSettingsQueryHandler : IQueryHandler +{ + private readonly PushMessageOptions _options; + + public GetFcmSettingsQueryHandler(IOptions options) + { + _options = options.Value; + } + + public Task Handle(GetFcmSettingsQuery request, CancellationToken cancellationToken) + { + var result = _options.UseFirebaseCloudMessaging + ? _options.FcmReceiverOptions + : null; + + return Task.FromResult(result); + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/FcmSettingsType.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/FcmSettingsType.cs new file mode 100644 index 0000000..90f8733 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/FcmSettingsType.cs @@ -0,0 +1,18 @@ +using GraphQL.Types; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Schemas; + +public class FcmSettingsType : ObjectGraphType +{ + public FcmSettingsType() + { + Field(x => x.ApiKey, nullable: false); + Field(x => x.AuthDomain, nullable: false); + Field(x => x.ProjectId, nullable: false); + Field(x => x.StorageBucket, nullable: false); + Field(x => x.MessagingSenderId, nullable: false); + Field(x => x.AppId, nullable: false); + Field(x => x.VapidKey, nullable: false); + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/InputAddFcmTokenType.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/InputAddFcmTokenType.cs new file mode 100644 index 0000000..fbcbe46 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/InputAddFcmTokenType.cs @@ -0,0 +1,12 @@ +using GraphQL.Types; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Schemas +{ + public class InputAddFcmTokenType : InputObjectGraphType + { + public InputAddFcmTokenType() + { + Field>("token"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/InputDeleteFcmTokenType.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/InputDeleteFcmTokenType.cs new file mode 100644 index 0000000..9debe9d --- /dev/null +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Schemas/InputDeleteFcmTokenType.cs @@ -0,0 +1,12 @@ +using GraphQL.Types; + +namespace VirtoCommerce.PushMessages.ExperienceApi.Schemas +{ + public class InputDeleteFcmTokenType : InputObjectGraphType + { + public InputDeleteFcmTokenType() + { + Field>("token"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json b/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json index 9a0d9a9..740afe4 100644 --- a/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json +++ b/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json @@ -11,6 +11,34 @@ "title": "Batch size", "description": "" }, + "PushMessages.FcmReceiverOptions.ApiKey": { + "title": "ApiKey", + "description": "" + }, + "PushMessages.FcmReceiverOptions.AuthDomain": { + "title": "AuthDomain", + "description": "" + }, + "PushMessages.FcmReceiverOptions.ProjectId": { + "title": "ProjectId", + "description": "" + }, + "PushMessages.FcmReceiverOptions.StorageBucket": { + "title": "StorageBucket", + "description": "" + }, + "PushMessages.FcmReceiverOptions.MessagingSenderId": { + "title": "MessagingSenderId", + "description": "" + }, + "PushMessages.FcmReceiverOptions.AppId": { + "title": "AppId", + "description": "" + }, + "PushMessages.FcmReceiverOptions.VapidKey": { + "title": "VapidKey", + "description": "" + }, "PushMessages.SendScheduledMessagesRecurringJob.Enable": { "title": "Send scheduled messages recurring job: enable", "description": "" diff --git a/src/VirtoCommerce.PushMessages.Web/Module.cs b/src/VirtoCommerce.PushMessages.Web/Module.cs index a38713d..6b870f2 100644 --- a/src/VirtoCommerce.PushMessages.Web/Module.cs +++ b/src/VirtoCommerce.PushMessages.Web/Module.cs @@ -16,6 +16,7 @@ using VirtoCommerce.PushMessages.Core; using VirtoCommerce.PushMessages.Core.BackgroundJobs; using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Core.Services; using VirtoCommerce.PushMessages.Data.BackgroundJobs; using VirtoCommerce.PushMessages.Data.Extensions; @@ -39,6 +40,8 @@ public class Module : IModule, IHasConfiguration public void Initialize(IServiceCollection serviceCollection) { + serviceCollection.AddOptions().BindConfiguration("PushMessages").ValidateDataAnnotations(); + serviceCollection.AddDbContext(options => { var databaseProvider = Configuration.GetValue("DatabaseProvider", "SqlServer"); @@ -67,6 +70,10 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -80,7 +87,7 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddAutoMapper(assemblyMarker); serviceCollection.AddSchemaBuilders(assemblyMarker); serviceCollection.AddDistributedMessageService(Configuration); - serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } @@ -104,8 +111,9 @@ public void PostInitialize(IApplicationBuilder appBuilder) // Register event handlers appBuilder.RegisterEventHandler(); appBuilder.RegisterEventHandler(); - appBuilder.RegisterEventHandler(); + appBuilder.RegisterEventHandler(); + appBuilder.UseFirebaseCloudMessaging(ModuleInfo.Id); appBuilder.UsePushMessageJobs(); }