diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index 34a9d61b..e4b3f607 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; using Moonlight.App.Database.Entities.Tickets; using Moonlight.App.Services; @@ -25,6 +26,12 @@ public class DataContext : DbContext public DbSet Coupons { get; set; } public DbSet CouponUses { get; set; } + + // Community + public DbSet Posts { get; set; } + public DbSet PostComments { get; set; } + public DbSet PostLikes { get; set; } + public DbSet WordFilters { get; set; } public DataContext(ConfigService configService) { diff --git a/Moonlight/App/Database/Entities/Community/Post.cs b/Moonlight/App/Database/Entities/Community/Post.cs new file mode 100644 index 00000000..2d4af22f --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/Post.cs @@ -0,0 +1,16 @@ +using Moonlight.App.Database.Enums; + +namespace Moonlight.App.Database.Entities.Community; + +public class Post +{ + public int Id { get; set; } + public string Title { get; set; } = ""; + public string Content { get; set; } = ""; + public User Author { get; set; } + public PostType Type { get; set; } + public List Comments { get; set; } = new(); + public List Likes { get; set; } = new(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/Community/PostComment.cs b/Moonlight/App/Database/Entities/Community/PostComment.cs new file mode 100644 index 00000000..633bf410 --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/PostComment.cs @@ -0,0 +1,10 @@ +namespace Moonlight.App.Database.Entities.Community; + +public class PostComment +{ + public int Id { get; set; } + public string Content { get; set; } = ""; + public User Author { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/Community/PostLike.cs b/Moonlight/App/Database/Entities/Community/PostLike.cs new file mode 100644 index 00000000..0d0085d1 --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/PostLike.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Database.Entities.Community; + +public class PostLike +{ + public int Id { get; set; } + public User User { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/Community/WordFilter.cs b/Moonlight/App/Database/Entities/Community/WordFilter.cs new file mode 100644 index 00000000..8d081159 --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/WordFilter.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Database.Entities.Community; + +public class WordFilter +{ + public int Id { get; set; } + public string Filter { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Enums/PostType.cs b/Moonlight/App/Database/Enums/PostType.cs new file mode 100644 index 00000000..4555a3b2 --- /dev/null +++ b/Moonlight/App/Database/Enums/PostType.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Database.Enums; + +public enum PostType +{ + Project = 0, + Announcement = 1, + Event = 2 +} \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.Designer.cs b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.Designer.cs new file mode 100644 index 00000000..b1a2f6e1 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.Designer.cs @@ -0,0 +1,539 @@ +// +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("20231027105412_AddPostsModels")] + partial class AddPostsModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.cs b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.cs new file mode 100644 index 00000000..7910ab27 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddPostsModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Posts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + AuthorId = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Posts", x => x.Id); + table.ForeignKey( + name: "FK_Posts_Users_AuthorId", + column: x => x.AuthorId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PostComments", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Content = table.Column(type: "TEXT", nullable: false), + AuthorId = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + PostId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PostComments", x => x.Id); + table.ForeignKey( + name: "FK_PostComments_Posts_PostId", + column: x => x.PostId, + principalTable: "Posts", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PostComments_Users_AuthorId", + column: x => x.AuthorId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PostLikes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + PostId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PostLikes", x => x.Id); + table.ForeignKey( + name: "FK_PostLikes_Posts_PostId", + column: x => x.PostId, + principalTable: "Posts", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PostLikes_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PostComments_AuthorId", + table: "PostComments", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_PostComments_PostId", + table: "PostComments", + column: "PostId"); + + migrationBuilder.CreateIndex( + name: "IX_PostLikes_PostId", + table: "PostLikes", + column: "PostId"); + + migrationBuilder.CreateIndex( + name: "IX_PostLikes_UserId", + table: "PostLikes", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Posts_AuthorId", + table: "Posts", + column: "AuthorId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PostComments"); + + migrationBuilder.DropTable( + name: "PostLikes"); + + migrationBuilder.DropTable( + name: "Posts"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231028214520_AddedWordFilter.Designer.cs b/Moonlight/App/Database/Migrations/20231028214520_AddedWordFilter.Designer.cs new file mode 100644 index 00000000..7f0deefd --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231028214520_AddedWordFilter.Designer.cs @@ -0,0 +1,554 @@ +// +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("20231028214520_AddedWordFilter")] + partial class AddedWordFilter + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.WordFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231028214520_AddedWordFilter.cs b/Moonlight/App/Database/Migrations/20231028214520_AddedWordFilter.cs new file mode 100644 index 00000000..4ff13e1e --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231028214520_AddedWordFilter.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedWordFilter : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "WordFilters", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Filter = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WordFilters", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "WordFilters"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 6cfcd7f2..d6c7ed52 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -17,6 +17,109 @@ namespace Moonlight.App.Database.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.WordFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => { b.Property("Id") @@ -299,6 +402,47 @@ namespace Moonlight.App.Database.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => { b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") @@ -381,6 +525,13 @@ namespace Moonlight.App.Database.Migrations .HasForeignKey("UserId"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => { b.Navigation("Shares"); diff --git a/Moonlight/App/Event/Events.cs b/Moonlight/App/Event/Events.cs index 35d822e4..d2967282 100644 --- a/Moonlight/App/Event/Events.cs +++ b/Moonlight/App/Event/Events.cs @@ -1,4 +1,5 @@ using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; using Moonlight.App.Event.Args; @@ -12,4 +13,10 @@ public class Events public static EventHandler OnUserMailVerify; public static EventHandler OnServiceOrdered; public static EventHandler OnTransactionCreated; + public static EventHandler OnPostCreated; + public static EventHandler OnPostUpdated; + public static EventHandler OnPostDeleted; + public static EventHandler OnPostLiked; + public static EventHandler OnPostCommentCreated; + public static EventHandler OnPostCommentDeleted; } \ No newline at end of file diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 2c4dc708..dadf0495 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -225,6 +225,34 @@ public static class Formatter } }; } + + public static string FormatAgoFromDateTime(DateTime dt) + { + TimeSpan timeSince = DateTime.UtcNow.Subtract(dt); + + if (timeSince.TotalMilliseconds < 1) + return "just now"; + + if (timeSince.TotalMinutes < 1) + return "less than a minute ago"; + + if (timeSince.TotalMinutes < 2) + return "1 minute ago"; + + if (timeSince.TotalMinutes < 60) + return Math.Round(timeSince.TotalMinutes) + " minutes ago"; + + if (timeSince.TotalHours < 2) + return "1 hour ago"; + + if (timeSince.TotalHours < 24) + return Math.Round(timeSince.TotalHours) + " hours ago"; + + if (timeSince.TotalDays < 2) + return "1 day ago"; + + return Math.Round(timeSince.TotalDays) + " days ago"; + } // This will replace every placeholder with the respective value if specified in the model // For example: diff --git a/Moonlight/App/Models/Enums/Permission.cs b/Moonlight/App/Models/Enums/Permission.cs index 3bb7e627..677bf9d8 100644 --- a/Moonlight/App/Models/Enums/Permission.cs +++ b/Moonlight/App/Models/Enums/Permission.cs @@ -9,6 +9,7 @@ public enum Permission AdminSessions = 1002, AdminUsersEdit = 1003, AdminTickets = 1004, + AdminCommunity = 1030, AdminStore = 1900, AdminViewExceptions = 1999, AdminRoot = 2000 diff --git a/Moonlight/App/Models/Forms/Admin/Community/AddWordFilter.cs b/Moonlight/App/Models/Forms/Admin/Community/AddWordFilter.cs new file mode 100644 index 00000000..b059a134 --- /dev/null +++ b/Moonlight/App/Models/Forms/Admin/Community/AddWordFilter.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.App.Models.Forms.Admin.Community; + +public class AddWordFilter +{ + [Required(ErrorMessage = "You need to specify a filter")] + [Description( + "This filters all posts and comments created using this regex. If any match is found it will block the action")] + public string Filter { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Admin/Community/EditWordFilter.cs b/Moonlight/App/Models/Forms/Admin/Community/EditWordFilter.cs new file mode 100644 index 00000000..c87cbc1a --- /dev/null +++ b/Moonlight/App/Models/Forms/Admin/Community/EditWordFilter.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.App.Models.Forms.Admin.Community; + +public class EditWordFilter +{ + [Required(ErrorMessage = "You need to specify a filter")] + [Description( + "This filters all posts and comments created using this regex. If any match is found it will block the action")] + public string Filter { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Community/AddPostForm.cs b/Moonlight/App/Models/Forms/Community/AddPostForm.cs new file mode 100644 index 00000000..c4dded80 --- /dev/null +++ b/Moonlight/App/Models/Forms/Community/AddPostForm.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonlight.App.Models.Forms.Community; + +public class AddPostForm +{ + [Required(ErrorMessage = "You need to enter a title")] + [MaxLength(40, ErrorMessage = "The title can only be 40 characters long")] + [MinLength(8, ErrorMessage = "The title must at least have 8 characters")] + public string Title { get; set; } = ""; + + [Required(ErrorMessage = "You need to enter post content")] + [MaxLength(2048, ErrorMessage = "The post content can only be 2048 characters long")] + [MinLength(8, ErrorMessage = "The post content must at least have 8 characters")] + public string Content { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Services/Community/PostService.cs b/Moonlight/App/Services/Community/PostService.cs new file mode 100644 index 00000000..f29b94ac --- /dev/null +++ b/Moonlight/App/Services/Community/PostService.cs @@ -0,0 +1,200 @@ +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Community; +using Moonlight.App.Database.Enums; +using Moonlight.App.Event; +using Moonlight.App.Exceptions; +using Moonlight.App.Extensions; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Community; + +public class PostService +{ + private readonly Repository PostRepository; + private readonly Repository PostLikeRepository; + private readonly Repository PostCommentRepository; + private readonly Repository WordFilterRepository; + + public PostService( + Repository postRepository, + Repository postLikeRepository, + Repository postCommentRepository, + Repository wordFilterRepository) + { + PostRepository = postRepository; + PostLikeRepository = postLikeRepository; + PostCommentRepository = postCommentRepository; + WordFilterRepository = wordFilterRepository; + } + + // Posts + public async Task Create(User user, string title, string content, PostType type) + { + if(await CheckTextForBadWords(title)) + throw new DisplayException("Bad word detected. Please follow the community rules"); + + if(await CheckTextForBadWords(content)) + throw new DisplayException("Bad word detected. Please follow the community rules"); + + var post = new Post() + { + Author = user, + Title = title, + Content = content, + Type = type + }; + + var finishedPost = PostRepository.Add(post); + + await Events.OnPostCreated.InvokeAsync(finishedPost); + + return finishedPost; + } + + public async Task Update(Post post, string title, string content) + { + if(await CheckTextForBadWords(title)) + throw new DisplayException("Bad word detected. Please follow the community rules"); + + if(await CheckTextForBadWords(content)) + throw new DisplayException("Bad word detected. Please follow the community rules"); + + post.Title = title; + post.Content = content; + post.UpdatedAt = DateTime.UtcNow; + + PostRepository.Update(post); + + await Events.OnPostUpdated.InvokeAsync(post); + } + + public async Task Delete(Post post) + { + var postWithData = PostRepository + .Get() + .Include(x => x.Comments) + .Include(x => x.Likes) + .First(x => x.Id == post.Id); + + // Cache relational data to delete later on + var likes = postWithData.Likes.ToArray(); + var comments = postWithData.Comments.ToArray(); + + // Clear relations + postWithData.Comments.Clear(); + postWithData.Likes.Clear(); + + PostRepository.Update(postWithData); + + // Delete relational data + foreach (var like in likes) + PostLikeRepository.Delete(like); + + foreach (var comment in comments) + PostCommentRepository.Delete(comment); + + // Now delete the post itself + PostRepository.Delete(post); + await Events.OnPostDeleted.InvokeAsync(post); + } + + // Comments + public async Task CreateComment(Post post, User user, string content) + { + // As the comment feature has no edit form or model to validate we do the validation here + if (string.IsNullOrEmpty(content)) + throw new DisplayException("Comment content cannot be empty"); + + if (content.Length > 1024) + throw new DisplayException("Comment content cannot be longer than 1024 characters"); + + if (!Regex.IsMatch(content, "^[ a-zA-Z0-9äöüßÄÖÜẞ,.;_\\n\\t-]+$")) + throw new DisplayException("Illegal characters in comment content"); + + if(await CheckTextForBadWords(content)) + throw new DisplayException("Bad word detected. Please follow the community rules"); + + //TODO: Swear word filter + + var comment = new PostComment() + { + Author = user, + Content = content + }; + + post.Comments.Add(comment); + PostRepository.Update(post); + + await Events.OnPostCommentCreated.InvokeAsync(comment); + + return comment; + } + + public async Task DeleteComment(Post post, PostComment comment) + { + var postWithComments = PostRepository + .Get() + .Include(x => x.Comments) + .First(x => x.Id == post.Id); + + var commentToRemove = postWithComments.Comments.First(x => x.Id == comment.Id); + postWithComments.Comments.Remove(commentToRemove); + + PostRepository.Update(postWithComments); + PostCommentRepository.Delete(commentToRemove); + + await Events.OnPostCommentCreated.InvokeAsync(commentToRemove); + } + + // Other + public async Task ToggleLike(Post post, User user) + { + var postWithLikes = PostRepository + .Get() + .Include(x => x.Likes) + .ThenInclude(x => x.User) + .First(x => x.Id == post.Id); + + var userLike = postWithLikes.Likes.FirstOrDefault(x => x.User.Id == user.Id); + + if (userLike != null) // Check if person already liked + { + postWithLikes.Likes.Remove(userLike); + + PostRepository.Update(postWithLikes); + PostLikeRepository.Delete(userLike); + } + else + { + postWithLikes.Likes.Add(new() + { + User = user + }); + + PostRepository.Update(postWithLikes); + + await Events.OnPostLiked.InvokeAsync(postWithLikes); + } + } + + // Utils + private Task CheckTextForBadWords(string input) // This method checks for bad words using the filters added by an admin + { + var filters = WordFilterRepository + .Get() + .Select(x => x.Filter) + .ToArray(); + + //TODO: Add timer for regex matching to create warnings + + foreach (var filter in filters) + { + if (Regex.IsMatch(input, filter)) + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Interop/ModalService.cs b/Moonlight/App/Services/Interop/ModalService.cs index 45d1984f..fcff73e2 100644 --- a/Moonlight/App/Services/Interop/ModalService.cs +++ b/Moonlight/App/Services/Interop/ModalService.cs @@ -11,11 +11,11 @@ public class ModalService JsRuntime = jsRuntime; } - public async Task Show(string id) + public async Task Show(string id, bool focus = true) // Focus can be specified to fix issues with other components { try { - await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id); + await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id, focus); } catch (Exception) { diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index ba132670..e6aec375 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -19,12 +19,12 @@ - + diff --git a/Moonlight/Pages/_Host.cshtml b/Moonlight/Pages/_Host.cshtml index 266723ca..305ab94a 100644 --- a/Moonlight/Pages/_Host.cshtml +++ b/Moonlight/Pages/_Host.cshtml @@ -37,6 +37,7 @@ + \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index fe9ebe16..dca2ceb4 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -8,6 +8,7 @@ using Moonlight.App.Helpers.LogMigrator; using Moonlight.App.Repositories; using Moonlight.App.Services; using Moonlight.App.Services.Background; +using Moonlight.App.Services.Community; using Moonlight.App.Services.Interop; using Moonlight.App.Services.ServiceManage; using Moonlight.App.Services.Store; @@ -58,6 +59,9 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); +// Services / Community +builder.Services.AddScoped(); + // Services / Users builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Components/Community/PostView.razor b/Moonlight/Shared/Components/Community/PostView.razor new file mode 100644 index 00000000..1ecaac4a --- /dev/null +++ b/Moonlight/Shared/Components/Community/PostView.razor @@ -0,0 +1,272 @@ +@using Moonlight.App.Database.Entities.Community +@using Ganss.Xss +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Models.Enums +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Moonlight.App.Services.Community + +@inject Repository PostRepository +@inject IdentityService IdentityService +@inject PostService PostService +@inject ToastService ToastService + +
+
+
+
+ +
+
+ @(Post.Author.Username) + @(Formatter.FormatAgoFromDateTime(Post.CreatedAt)) +
+
+ + @if (Post.Author.Id == IdentityService.CurrentUser.Id || IdentityService.Permissions[Permission.AdminCommunity]) + { + + } +
+
+
+ @if (IsEditing) + { + + } + else + { + var sanitizer = new HtmlSanitizer(); + var content = sanitizer.Sanitize(Post.Content); + + @((MarkupString)content) + } +
+
+ +
+ +@code +{ + [Parameter] + public Post Post { get; set; } + + [Parameter] + public Func? OnUpdate { get; set; } + + private int CommentsCount = -1; + private int LikesCount = -1; + private bool HasLiked = false; + + private bool ShowComments = false; + private PostComment[] Comments = Array.Empty(); + private string Comment = ""; + + private bool IsEditing = false; + private string EditTitle = ""; + private string EditContent = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await UpdateCounts(); + } + } + + private Task LoadComments(LazyLoader _) + { + Comments = PostRepository + .Get() + .Include(x => x.Comments) + .ThenInclude(x => x.Author) + .First(x => x.Id == Post.Id) + .Comments + .OrderBy(x => x.CreatedAt) + .ToArray(); + + return Task.CompletedTask; + } + + private async Task UpdateCounts() + { + CommentsCount = PostRepository + .Get() + .Where(x => x.Id == Post.Id) + .SelectMany(x => x.Comments) + .Count(); + + LikesCount = PostRepository + .Get() + .Where(x => x.Id == Post.Id) + .SelectMany(x => x.Likes) + .Count(); + + HasLiked = PostRepository + .Get() + .Where(x => x.Id == Post.Id) + .SelectMany(x => x.Likes) + .Any(x => x.User.Id == IdentityService.CurrentUser.Id); + + await InvokeAsync(StateHasChanged); + } + + private async Task CreateComment() + { + await PostService.CreateComment(Post, IdentityService.CurrentUser, Comment); + + Comment = ""; + ShowComments = true; + await LoadComments(null!); + await InvokeAsync(StateHasChanged); + await UpdateCounts(); + } + + private async Task DeleteComment(PostComment comment) + { + await PostService.DeleteComment(Post, comment); + + await LoadComments(null!); + await InvokeAsync(StateHasChanged); + await UpdateCounts(); + + await ToastService.Success("Successfully deleted comment"); + } + + private async Task ToggleComments() + { + ShowComments = !ShowComments; + await InvokeAsync(StateHasChanged); + + if (!ShowComments) + Comments = Array.Empty(); // Clear unused data + } + + private async Task ToggleLike() + { + await PostService.ToggleLike(Post, IdentityService.CurrentUser); + await UpdateCounts(); + } + + private async Task ToggleEdit(bool preventSaving = false) + { + IsEditing = !IsEditing; + + if (IsEditing) + { + EditTitle = Post.Title; + EditContent = Post.Content; + } + else if (!preventSaving) + { + await PostService.Update(Post, EditTitle, EditContent); + await ToastService.Success("Successfully saved post"); + } + + await InvokeAsync(StateHasChanged); + } + + private async Task DeletePost() + { + await PostService.Delete(Post); + await ToastService.Success("Successfully deleted post"); + + if (OnUpdate != null) + await OnUpdate.Invoke(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Forms/AutoCrud.razor b/Moonlight/Shared/Components/Forms/AutoCrud.razor index 15e4dd13..e6b5bcc3 100644 --- a/Moonlight/Shared/Components/Forms/AutoCrud.razor +++ b/Moonlight/Shared/Components/Forms/AutoCrud.razor @@ -54,7 +54,7 @@