diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index 2a18b2d0..3a352938 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -45,6 +45,7 @@ public class DataContext : DbContext public DbSet CloudPanels { get; set; } public DbSet Databases { get; set; } public DbSet WebSpaces { get; set; } + public DbSet SupportChatMessages { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/Moonlight/App/Database/Entities/SupportChatMessage.cs b/Moonlight/App/Database/Entities/SupportChatMessage.cs new file mode 100644 index 00000000..47ad3e6b --- /dev/null +++ b/Moonlight/App/Database/Entities/SupportChatMessage.cs @@ -0,0 +1,21 @@ +using Moonlight.App.Models.Misc; + +namespace Moonlight.App.Database.Entities; + +public class SupportChatMessage +{ + public int Id { get; set; } + + public string Content { get; set; } = ""; + public string Attachment { get; set; } = ""; + + public User? Sender { get; set; } + public User Recipient { get; set; } + + public bool IsQuestion { get; set; } = false; + public QuestionType QuestionType { get; set; } + public string Answer { get; set; } = ""; + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs b/Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs new file mode 100644 index 00000000..7cea0436 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs @@ -0,0 +1,1087 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.App.Database; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20230420213846_AddedNewSupportChatModels")] + partial class AddedNewSupportChatModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ApiUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Host") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("CloudPanels"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("bigint"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Ongoing") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.ToTable("DdosAttacks"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("SharedDomainId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("SharedDomainId"); + + b.ToTable("Domains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Allocations") + .HasColumnType("int"); + + b.Property("ConfigFiles") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallEntrypoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Startup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LoadingMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AuditLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Class") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Stacktrace") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("ErrorLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SecurityLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WebSpaceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WebSpaceId"); + + b.ToTable("Databases"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NewsEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("NewsEntries"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("MoonlightDaemonPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SftpPort") + .HasColumnType("int"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("NodeAllocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NotificationClientId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NotificationClientId"); + + b.ToTable("NotificationActions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationClients"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Identifier") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Revokes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("Disk") + .HasColumnType("bigint"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Installing") + .HasColumnType("tinyint(1)"); + + b.Property("IsCleanupException") + .HasColumnType("tinyint(1)"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Suspended") + .HasColumnType("tinyint(1)"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bytes") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudflareId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("SharedDomains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.StatisticsData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Chart") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("Statistics"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LimitsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Attachment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("QuestionType") + .HasColumnType("int"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportChatMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("IsSupport") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Admin") + .HasColumnType("tinyint(1)"); + + b.Property("City") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Country") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentSubscriptionId") + .HasColumnType("int"); + + b.Property("DiscordId") + .HasColumnType("bigint unsigned"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SubscriptionDuration") + .HasColumnType("int"); + + b.Property("SubscriptionSince") + .HasColumnType("datetime(6)"); + + b.Property("SupportPending") + .HasColumnType("tinyint(1)"); + + b.Property("TokenValidTime") + .HasColumnType("datetime(6)"); + + b.Property("TotpEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurrentSubscriptionId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudPanelId") + .HasColumnType("int"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VHostTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CloudPanelId"); + + b.HasIndex("OwnerId"); + + b.ToTable("WebSpaces"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("DockerImages") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.SharedDomain", "SharedDomain") + .WithMany() + .HasForeignKey("SharedDomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SharedDomain"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("Variables") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.HasOne("Moonlight.App.Database.Entities.WebSpace", "WebSpace") + .WithMany("Databases") + .HasForeignKey("WebSpaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WebSpace"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", null) + .WithMany("Allocations") + .HasForeignKey("NodeId"); + + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.HasOne("Moonlight.App.Database.Entities.Notification.NotificationClient", "NotificationClient") + .WithMany() + .HasForeignKey("NotificationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationClient"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.NodeAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId"); + + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Backups") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") + .WithMany() + .HasForeignKey("RecipientId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription") + .WithMany() + .HasForeignKey("CurrentSubscriptionId"); + + b.Navigation("CurrentSubscription"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.HasOne("Moonlight.App.Database.Entities.CloudPanel", "CloudPanel") + .WithMany() + .HasForeignKey("CloudPanelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CloudPanel"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Backups"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Navigation("Databases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.cs b/Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.cs new file mode 100644 index 00000000..e8160af4 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.cs @@ -0,0 +1,138 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedNewSupportChatModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Websites"); + + migrationBuilder.DropTable( + name: "PleskServers"); + + migrationBuilder.CreateTable( + name: "SupportChatMessages", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Content = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Attachment = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SenderId = table.Column(type: "int", nullable: true), + RecipientId = table.Column(type: "int", nullable: false), + IsQuestion = table.Column(type: "tinyint(1)", nullable: false), + QuestionType = table.Column(type: "int", nullable: false), + Answer = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SupportChatMessages", x => x.Id); + table.ForeignKey( + name: "FK_SupportChatMessages_Users_RecipientId", + column: x => x.RecipientId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SupportChatMessages_Users_SenderId", + column: x => x.SenderId, + principalTable: "Users", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_SupportChatMessages_RecipientId", + table: "SupportChatMessages", + column: "RecipientId"); + + migrationBuilder.CreateIndex( + name: "IX_SupportChatMessages_SenderId", + table: "SupportChatMessages", + column: "SenderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SupportChatMessages"); + + migrationBuilder.CreateTable( + name: "PleskServers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ApiKey = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ApiUrl = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_PleskServers", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Websites", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + OwnerId = table.Column(type: "int", nullable: false), + PleskServerId = table.Column(type: "int", nullable: false), + BaseDomain = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + FtpLogin = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + FtpPassword = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + PleskId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Websites", x => x.Id); + table.ForeignKey( + name: "FK_Websites_PleskServers_PleskServerId", + column: x => x.PleskServerId, + principalTable: "PleskServers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Websites_Users_OwnerId", + column: x => x.OwnerId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Websites_OwnerId", + table: "Websites", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Websites_PleskServerId", + table: "Websites", + column: "PleskServerId"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 88d74810..26b48d2e 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -453,29 +453,6 @@ namespace Moonlight.App.Database.Migrations b.ToTable("NotificationClients"); }); - modelBuilder.Entity("Moonlight.App.Database.Entities.PleskServer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - b.Property("ApiKey") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ApiUrl") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("PleskServers"); - }); - modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b => { b.Property("Id") @@ -673,6 +650,51 @@ namespace Moonlight.App.Database.Migrations b.ToTable("Subscriptions"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Attachment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("QuestionType") + .HasColumnType("int"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportChatMessages"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => { b.Property("Id") @@ -836,42 +858,6 @@ namespace Moonlight.App.Database.Migrations b.ToTable("WebSpaces"); }); - modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - b.Property("BaseDomain") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("FtpLogin") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("FtpPassword") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("OwnerId") - .HasColumnType("int"); - - b.Property("PleskId") - .HasColumnType("int"); - - b.Property("PleskServerId") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.HasIndex("PleskServerId"); - - b.ToTable("Websites"); - }); - modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => { b.HasOne("Moonlight.App.Database.Entities.Node", "Node") @@ -1007,6 +993,23 @@ namespace Moonlight.App.Database.Migrations .HasForeignKey("ServerId"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => { b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") @@ -1050,25 +1053,6 @@ namespace Moonlight.App.Database.Migrations b.Navigation("Owner"); }); - modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b => - { - b.HasOne("Moonlight.App.Database.Entities.User", "Owner") - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Moonlight.App.Database.Entities.PleskServer", "PleskServer") - .WithMany() - .HasForeignKey("PleskServerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Owner"); - - b.Navigation("PleskServer"); - }); - modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => { b.Navigation("DockerImages"); diff --git a/Moonlight/App/Events/EventSystem.cs b/Moonlight/App/Events/EventSystem.cs index eb5b12da..377e9eb3 100644 --- a/Moonlight/App/Events/EventSystem.cs +++ b/Moonlight/App/Events/EventSystem.cs @@ -80,7 +80,15 @@ public class EventSystem var del = (Delegate)subscriber.Action; - ((Task)del.DynamicInvoke(data)!).Wait(); + try + { + ((Task)del.DynamicInvoke(data)!).Wait(); + } + catch (Exception e) + { + Logger.Warn($"Error emitting '{subscriber.Id} on {subscriber.Handle}'"); + Logger.Warn(e); + } stopWatch.Stop(); diff --git a/Moonlight/App/Services/DiscordNotificationService.cs b/Moonlight/App/Services/DiscordNotificationService.cs new file mode 100644 index 00000000..0fc0a0c6 --- /dev/null +++ b/Moonlight/App/Services/DiscordNotificationService.cs @@ -0,0 +1,101 @@ +using Discord; +using Discord.Webhook; +using Logging.Net; +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; + +namespace Moonlight.App.Services; + +public class DiscordNotificationService +{ + private readonly EventSystem Event; + private readonly ResourceService ResourceService; + private readonly DiscordWebhookClient Client; + private readonly string AppUrl; + + public DiscordNotificationService( + EventSystem eventSystem, + ConfigService configService, + ResourceService resourceService) + { + Event = eventSystem; + ResourceService = resourceService; + + var config = configService.GetSection("Moonlight").GetSection("DiscordNotifications"); + + if (config.GetValue("Enable")) + { + Logger.Info("Discord notifications enabled"); + + Client = new(config.GetValue("WebHook")); + AppUrl = configService.GetSection("Moonlight").GetValue("AppUrl"); + + Event.On("supportChat.new", this, OnNewSupportChat); + Event.On("supportChat.message", this, OnSupportChatMessage); + Event.On("supportChat.close", this, OnSupportChatClose); + } + else + { + Logger.Info("Discord notifications disabled"); + } + } + + private async Task OnSupportChatClose(User user) + { + await SendNotification("", builder => + { + builder.Title = "A new support chat has been marked as closed"; + builder.Color = Color.Red; + builder.AddField("Email", user.Email); + builder.AddField("Firstname", user.FirstName); + builder.AddField("Lastname", user.LastName); + builder.Url = $"{AppUrl}/admin/support/view/{user.Id}"; + }); + } + + private async Task OnSupportChatMessage(SupportChatMessage message) + { + if(message.Sender == null) + return; + + await SendNotification("", builder => + { + builder.Title = "New message in support chat"; + builder.Color = Color.Blue; + builder.AddField("Message", message.Content); + builder.Author = new EmbedAuthorBuilder() + .WithName($"{message.Sender.FirstName} {message.Sender.LastName}") + .WithIconUrl(ResourceService.Avatar(message.Sender)); + builder.Url = $"{AppUrl}/admin/support/view/{message.Recipient.Id}"; + }); + } + + private async Task OnNewSupportChat(User user) + { + await SendNotification("", builder => + { + builder.Title = "A new support chat has been marked as active"; + builder.Color = Color.Green; + builder.AddField("Email", user.Email); + builder.AddField("Firstname", user.FirstName); + builder.AddField("Lastname", user.LastName); + builder.Url = $"{AppUrl}/admin/support/view/{user.Id}"; + }); + } + + private async Task SendNotification(string content, Action? embed = null) + { + Logger.Debug(Client); + + var e = new EmbedBuilder(); + embed?.Invoke(e); + + await Client.SendMessageAsync( + content, + false, + new []{e.Build()}, + "Moonlight Notification", + ResourceService.Image("logo.svg") + ); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs b/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs new file mode 100644 index 00000000..2f8307d0 --- /dev/null +++ b/Moonlight/App/Services/SupportChat/SupportChatAdminService.cs @@ -0,0 +1,127 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; +using Moonlight.App.Services.Sessions; + +namespace Moonlight.App.Services.SupportChat; + +public class SupportChatAdminService +{ + private readonly EventSystem Event; + private readonly IdentityService IdentityService; + private readonly SupportChatServerService ServerService; + + public Func? OnMessage { get; set; } + public Func? OnTypingChanged { get; set; } + + private User? User; + private User Recipient = null!; + private readonly List TypingUsers = new(); + + public SupportChatAdminService( + EventSystem eventSystem, + SupportChatServerService serverService, + IdentityService identityService) + { + Event = eventSystem; + ServerService = serverService; + IdentityService = identityService; + } + + public async Task Start(User recipient) + { + User = await IdentityService.Get(); + Recipient = recipient; + + if (User != null) + { + await Event.On($"supportChat.{Recipient.Id}.message", this, async message => + { + if(OnMessage != null) + await OnMessage.Invoke(message); + }); + + await Event.On($"supportChat.{Recipient.Id}.typing", this, async user => + { + await HandleTyping(user); + }); + } + } + + public async Task GetMessages() + { + if (User == null) + return Array.Empty(); + + return await ServerService.GetMessages(Recipient); + } + + public async Task SendMessage(string content) + { + if (User != null) + { + await ServerService.SendMessage(Recipient, content, User); + } + } + + private Task HandleTyping(User user) + { + lock (TypingUsers) + { + if (!TypingUsers.Contains(user)) + { + TypingUsers.Add(user); + + if (OnTypingChanged != null) + { + OnTypingChanged.Invoke( + TypingUsers + .Where(x => x.Id != User!.Id) + .Select(x => $"{x.FirstName} {x.LastName}") + .ToArray() + ); + } + + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(5)); + + if (TypingUsers.Contains(user)) + { + TypingUsers.Remove(user); + + if (OnTypingChanged != null) + { + await OnTypingChanged.Invoke( + TypingUsers + .Where(x => x.Id != User!.Id) + .Select(x => $"{x.FirstName} {x.LastName}") + .ToArray() + ); + } + } + }); + } + } + + return Task.CompletedTask; + } + + public async Task SendTyping() + { + await Event.Emit($"supportChat.{Recipient.Id}.typing", User); + } + + public async Task Close() + { + await ServerService.CloseChat(Recipient); + } + + public async void Dispose() + { + if (User != null) + { + await Event.Off($"supportChat.{Recipient.Id}.message", this); + await Event.Off($"supportChat.{Recipient.Id}.typing", this); + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/SupportChat/SupportChatClientService.cs b/Moonlight/App/Services/SupportChat/SupportChatClientService.cs new file mode 100644 index 00000000..8c3bd744 --- /dev/null +++ b/Moonlight/App/Services/SupportChat/SupportChatClientService.cs @@ -0,0 +1,121 @@ +using Logging.Net; +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; +using Moonlight.App.Services.Sessions; + +namespace Moonlight.App.Services.SupportChat; + +public class SupportChatClientService : IDisposable +{ + private readonly EventSystem Event; + private readonly IdentityService IdentityService; + private readonly SupportChatServerService ServerService; + + public Func? OnMessage { get; set; } + public Func? OnTypingChanged { get; set; } + + private User? User; + private readonly List TypingUsers = new(); + + public SupportChatClientService( + EventSystem eventSystem, + SupportChatServerService serverService, + IdentityService identityService) + { + Event = eventSystem; + ServerService = serverService; + IdentityService = identityService; + } + + public async Task Start() + { + User = await IdentityService.Get(); + + if (User != null) + { + await Event.On($"supportChat.{User.Id}.message", this, async message => + { + if(OnMessage != null) + await OnMessage.Invoke(message); + }); + + await Event.On($"supportChat.{User.Id}.typing", this, async user => + { + await HandleTyping(user); + }); + } + } + + public async Task GetMessages() + { + if (User == null) + return Array.Empty(); + + return await ServerService.GetMessages(User); + } + + public async Task SendMessage(string content) + { + if (User != null) + { + await ServerService.SendMessage(User, content, User); + } + } + + private Task HandleTyping(User user) + { + lock (TypingUsers) + { + if (!TypingUsers.Contains(user)) + { + TypingUsers.Add(user); + + if (OnTypingChanged != null) + { + OnTypingChanged.Invoke( + TypingUsers + .Where(x => x.Id != User!.Id) + .Select(x => $"{x.FirstName} {x.LastName}") + .ToArray() + ); + } + + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(5)); + + if (TypingUsers.Contains(user)) + { + TypingUsers.Remove(user); + + if (OnTypingChanged != null) + { + await OnTypingChanged.Invoke( + TypingUsers + .Where(x => x.Id != User!.Id) + .Select(x => $"{x.FirstName} {x.LastName}") + .ToArray() + ); + } + } + }); + } + } + + return Task.CompletedTask; + } + + public async Task SendTyping() + { + await Event.Emit($"supportChat.{User!.Id}.typing", User); + } + + public async void Dispose() + { + if (User != null) + { + await Event.Off($"supportChat.{User.Id}.message", this); + await Event.Off($"supportChat.{User.Id}.typing", this); + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/SupportChat/SupportChatServerService.cs b/Moonlight/App/Services/SupportChat/SupportChatServerService.cs new file mode 100644 index 00000000..c9bf5a4d --- /dev/null +++ b/Moonlight/App/Services/SupportChat/SupportChatServerService.cs @@ -0,0 +1,144 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.SupportChat; + +public class SupportChatServerService +{ + private readonly IServiceScopeFactory ServiceScopeFactory; + private readonly DateTimeService DateTimeService; + private readonly EventSystem Event; + + public SupportChatServerService( + IServiceScopeFactory serviceScopeFactory, + DateTimeService dateTimeService, + EventSystem eventSystem) + { + ServiceScopeFactory = serviceScopeFactory; + DateTimeService = dateTimeService; + Event = eventSystem; + } + + public Task GetMessages(User recipient) + { + using var scope = ServiceScopeFactory.CreateScope(); + var msgRepo = scope.ServiceProvider.GetRequiredService>(); + + var messages = msgRepo + .Get() + .Include(x => x.Recipient) + .Include(x => x.Sender) + .Where(x => x.Recipient.Id == recipient.Id) + .OrderByDescending(x => x.CreatedAt) + .AsEnumerable() + .Take(50) + .ToArray(); + + return Task.FromResult(messages); + } + + public async Task SendMessage(User recipient, string content, User? sender, string? attachment = null) + { + using var scope = ServiceScopeFactory.CreateScope(); + var msgRepo = scope.ServiceProvider.GetRequiredService>(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + var message = new SupportChatMessage() + { + CreatedAt = DateTimeService.GetCurrent(), + IsQuestion = false, + Sender = sender == null ? null : userRepo.Get().First(x => x.Id == sender.Id), + Recipient = userRepo.Get().First(x => x.Id == recipient.Id), + Answer = "", + Attachment = attachment ?? "", + Content = content, + UpdatedAt = DateTimeService.GetCurrent() + }; + + var finalMessage = msgRepo.Add(message); + + await Event.Emit($"supportChat.{recipient.Id}.message", finalMessage); + await Event.Emit("supportChat.message", finalMessage); + + if (!userRepo.Get().First(x => x.Id == recipient.Id).SupportPending) + { + var ticketStart = new SupportChatMessage() + { + CreatedAt = DateTimeService.GetCurrent(), + IsQuestion = false, + Sender = null, + Recipient = userRepo.Get().First(x => x.Id == recipient.Id), + Answer = "", + Attachment = "", + Content = "Support ticket open", //TODO: Config + UpdatedAt = DateTimeService.GetCurrent() + }; + + var ticketStartFinal = msgRepo.Add(ticketStart); + + var user = userRepo.Get().First(x => x.Id == recipient.Id); + user.SupportPending = true; + userRepo.Update(user); + + await Event.Emit($"supportChat.{recipient.Id}.message", ticketStartFinal); + await Event.Emit("supportChat.message", ticketStartFinal); + await Event.Emit("supportChat.new", recipient); + } + } + + public Task> GetOpenChats() + { + var result = new Dictionary(); + + using var scope = ServiceScopeFactory.CreateScope(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + var msgRepo = scope.ServiceProvider.GetRequiredService>(); + + foreach (var user in userRepo.Get().Where(x => x.SupportPending).ToArray()) + { + var lastMessage = msgRepo + .Get() + .Include(x => x.Recipient) + .Include(x => x.Sender) + .Where(x => x.Recipient.Id == user.Id) + .OrderByDescending(x => x.CreatedAt) + .AsEnumerable() + .FirstOrDefault(); + + result.Add(user, lastMessage); + } + + return Task.FromResult(result); + } + + public async Task CloseChat(User recipient) + { + using var scope = ServiceScopeFactory.CreateScope(); + var msgRepo = scope.ServiceProvider.GetRequiredService>(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + var ticketEnd = new SupportChatMessage() + { + CreatedAt = DateTimeService.GetCurrent(), + IsQuestion = false, + Sender = null, + Recipient = userRepo.Get().First(x => x.Id == recipient.Id), + Answer = "", + Attachment = "", + Content = "Support ticket closed", //TODO: Config + UpdatedAt = DateTimeService.GetCurrent() + }; + + var ticketEndFinal = msgRepo.Add(ticketEnd); + + var user = userRepo.Get().First(x => x.Id == recipient.Id); + user.SupportPending = false; + userRepo.Update(user); + + await Event.Emit($"supportChat.{recipient.Id}.message", ticketEndFinal); + await Event.Emit("supportChat.message", ticketEndFinal); + await Event.Emit("supportChat.close", recipient); + } +} \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index ca3ac6c7..5c375652 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index a8a470b9..77c21a0e 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -20,6 +20,7 @@ using Moonlight.App.Services.OAuth2; using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Statistics; using Moonlight.App.Services.Support; +using Moonlight.App.Services.SupportChat; namespace Moonlight { @@ -124,10 +125,15 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddSingleton(); - // Support + // Support TODO: Remove builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + + // Support chat + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // Helpers builder.Services.AddSingleton(); @@ -143,6 +149,7 @@ namespace Moonlight // Background services builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Third party services builder.Services.AddBlazorTable(); @@ -176,6 +183,7 @@ namespace Moonlight _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); + _ = app.Services.GetRequiredService(); // Discord bot service //var discordBotService = app.Services.GetRequiredService(); diff --git a/Moonlight/Shared/Views/Admin/Support/Index.razor b/Moonlight/Shared/Views/Admin/Support/Index.razor index 355a863a..ede64b7d 100644 --- a/Moonlight/Shared/Views/Admin/Support/Index.razor +++ b/Moonlight/Shared/Views/Admin/Support/Index.razor @@ -1,13 +1,11 @@ @page "/admin/support" -@using Moonlight.App.Repositories @using Moonlight.App.Database.Entities -@using Microsoft.EntityFrameworkCore -@using Moonlight.App.Database -@using Moonlight.App.Services +@using Moonlight.App.Events +@using Moonlight.App.Services.SupportChat + +@inject SupportChatServerService ServerService +@inject EventSystem Event -@inject SupportMessageRepository SupportMessageRepository -@inject ConfigService ConfigService -@inject MessageService MessageService @implements IDisposable @@ -18,13 +16,13 @@

- Open tickets + Open chats

- @if (Users.Any()) + @if (OpenChats.Any()) { - foreach (var user in Users) + foreach (var chat in OpenChats) {
@@ -35,25 +33,21 @@ @@ -66,7 +60,7 @@ else {
- No support ticket is currently open + No support chat is currently open
} @@ -80,53 +74,28 @@ @code { - private User[] Users; - private Dictionary MessageCache; - private LazyLoader? LazyLoader; + private Dictionary OpenChats = new(); - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - MessageCache = new(); - - MessageService.Subscribe("support.new", this, async user => + await Event.On("supportChat.new", this, async user => { - if (LazyLoader != null) - await LazyLoader.Reload(); - }); - - MessageService.Subscribe("support.close", this, async user => - { - if (LazyLoader != null) - await LazyLoader.Reload(); + //TODO: Play sound or smth. Add a config option + + OpenChats = await ServerService.GetOpenChats(); + + await InvokeAsync(StateHasChanged); }); } - private Task Load(LazyLoader arg) + private async Task Load(LazyLoader arg) // Only for initial load { - // We dont want cache here - Users = (new UserRepository(new DataContext(ConfigService))) - .Get() - .Where(x => x.SupportPending) - .ToArray(); - - foreach (var user in Users) - { - var lastMessage = SupportMessageRepository - .Get() - .Include(x => x.Recipient) - .OrderByDescending(x => x.Id) - .FirstOrDefault(x => x.Recipient!.Id == user.Id); - - MessageCache.Add(user, lastMessage); - } - - return Task.CompletedTask; + OpenChats = await ServerService.GetOpenChats(); } - public void Dispose() + public async void Dispose() { - MessageService.Unsubscribe("support.new", this); - MessageService.Unsubscribe("support.close", this); + await Event.Off("supportChat.new", this); } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Support/View.razor b/Moonlight/Shared/Views/Admin/Support/View.razor index d114d935..87c4c683 100644 --- a/Moonlight/Shared/Views/Admin/Support/View.razor +++ b/Moonlight/Shared/Views/Admin/Support/View.razor @@ -1,11 +1,11 @@ @page "/admin/support/view/{Id:int}" -@using Moonlight.App.Services.Support @using Moonlight.App.Database.Entities @using Moonlight.App.Helpers @using Moonlight.App.Repositories @using Moonlight.App.Services +@using Moonlight.App.Services.SupportChat -@inject SupportAdminService SupportAdminService +@inject SupportChatAdminService AdminService @inject UserRepository UserRepository @inject SmartTranslateService SmartTranslateService @inject ResourceService ResourceService @@ -29,7 +29,7 @@
@foreach (var message in Messages) { - if (message.IsSystem || message.IsSupport) + if (message.Sender == null || message.Sender.Id != User.Id) {
@@ -37,29 +37,29 @@ -
+
Logo
- @if (message.IsSystem) + @if (message.Sender == null) { - @(message.Message) + @(message.Content) } else { - @(message.Message) + @(message.Content) }
@@ -75,14 +75,14 @@
- @(message.Message) + @(message.Content)
@@ -92,11 +92,7 @@
- Email: @(User.Email) + Email: @(User.Email)
@@ -183,9 +179,11 @@ public int Id { get; set; } private User? User; - private SupportMessage[] Messages; - private string Content = ""; + private SupportChatMessage[] Messages = Array.Empty(); + private string[] Typing = Array.Empty(); + + private string Content = ""; private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10); private async Task Load(LazyLoader arg) @@ -196,55 +194,54 @@ if (User != null) { - SupportAdminService.OnNewMessage += OnNewMessage; - SupportAdminService.OnUpdateTyping += OnUpdateTyping; + AdminService.OnMessage += OnMessage; + AdminService.OnTypingChanged += OnTypingChanged; - await SupportAdminService.Start(User); + await AdminService.Start(User); } } - #region Message handling - - private async void OnNewMessage(object? sender, SupportMessage e) + private async Task LoadMessages(LazyLoader arg) { - Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray(); + Messages = await AdminService.GetMessages(); + } + + private async Task OnTypingChanged(string[] typing) + { + Typing = typing; + await InvokeAsync(StateHasChanged); } - private async Task LoadMessages(LazyLoader arg) + private async Task OnMessage(SupportChatMessage arg) { - Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray(); + Messages = await AdminService.GetMessages(); + + //TODO: Sound when message from system or admin + + await InvokeAsync(StateHasChanged); } - #endregion - private async Task Send() { - await SupportAdminService.SendMessage(Content); + await AdminService.SendMessage(Content); Content = ""; + + await InvokeAsync(StateHasChanged); } private async Task CloseTicket() { - await SupportAdminService.Close(); + await AdminService.Close(); } - #region Typing - private async Task OnTyping() { if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5) { LastTypingTimestamp = DateTime.UtcNow; - await SupportAdminService.TriggerTyping(); + await AdminService.SendTyping(); } } - - private async void OnUpdateTyping(object? sender, EventArgs e) - { - await InvokeAsync(StateHasChanged); - } - - #endregion } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Support.razor b/Moonlight/Shared/Views/Support.razor index 9ef48cf8..1c0ee112 100644 --- a/Moonlight/Shared/Views/Support.razor +++ b/Moonlight/Shared/Views/Support.razor @@ -2,10 +2,10 @@ @using Moonlight.App.Services @using Moonlight.App.Database.Entities @using Moonlight.App.Helpers -@using Moonlight.App.Services.Support +@using Moonlight.App.Services.SupportChat @inject ResourceService ResourceService -@inject SupportClientService SupportClientService +@inject SupportChatClientService ClientService @inject SmartTranslateService SmartTranslateService @@ -16,7 +16,7 @@
@foreach (var message in Messages) { - if (message.IsSystem || message.IsSupport) + if (message.Sender == null || message.Sender.Id != User.Id) {
@@ -26,13 +26,13 @@
- @if (message.IsSystem) + @if (message.Sender == null) { - @(message.Message) + @(message.Content) } else { - @(message.Message) + @(message.Content) }
@@ -60,7 +60,7 @@
@@ -69,7 +69,7 @@
- @(message.Message) + @(message.Content)
@@ -83,7 +83,7 @@ @@ -97,11 +97,7 @@
- - @(user.FirstName) @(user.LastName) + + @(chat.Key.FirstName) @(chat.Key.LastName)
- @{ - var lastMessage = MessageCache.ContainsKey(user) ? MessageCache[user] : null; - } - - @if (lastMessage == null) + @if (chat.Value == null) { No message sent yet } else { - @(lastMessage.Message) + @(chat.Value.Content) }