From 069b0cdd408b8826a05b9ceb344833e865ed0618 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 25 Jun 2024 12:32:03 -0400 Subject: [PATCH 1/3] Added YouTube doc for quota expansion --- doc/YouTube-Doc.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/YouTube-Doc.txt diff --git a/doc/YouTube-Doc.txt b/doc/YouTube-Doc.txt new file mode 100644 index 00000000..2bc51bb6 --- /dev/null +++ b/doc/YouTube-Doc.txt @@ -0,0 +1,5 @@ +TagzApp is an open source application with all source code available for contributors and providers to review. We have created multiple 'providers' for various social media services including Mastodon, X, Twitch, Bluesky, and YouTube. + +TagzApp polls these services repeatedly in order to present message content on a unified dashboard for moderators and pop-up messages for live stream hosts and viewers to interact with + +You can review all of our C# source code for the YouTube provider at: https://github.com/FritzAndFriends/TagzApp/tree/main/src/TagzApp.Providers.YouTubeChat \ No newline at end of file From 2340024b5f7be326c554bcfe0d1987a237317247 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 25 Jun 2024 12:58:09 -0400 Subject: [PATCH 2/3] Added BlockUser Capabilities --- scripts/AddMigration.cmd | 2 +- src/TagzApp.Common/IModerationRepository.cs | 2 +- src/TagzApp.Common/Models/BlockedUser.cs | 10 + ...553_AddBlockedUserCapabilities.Designer.cs | 194 +++++++++++++++++ ...240625164553_AddBlockedUserCapabilities.cs | 29 +++ ...lockedUserCapabilitiesDefaults.Designer.cs | 198 ++++++++++++++++++ ...5165050_BlockedUserCapabilitiesDefaults.cs | 45 ++++ ...RemovedBlockedUserAlternateKey.Designer.cs | 196 +++++++++++++++++ ...25165405_RemovedBlockedUserAlternateKey.cs | 27 +++ .../Migrations/TagzAppContextModelSnapshot.cs | 9 +- src/TagzApp.Storage.Postgres/PgBlockedUser.cs | 5 +- .../PostgresModerationRepository.cs | 5 +- .../TagzApp.Storage.Postgres.csproj | 4 + .../TagzAppContext.cs | 13 +- 14 files changed, 731 insertions(+), 8 deletions(-) create mode 100644 src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.Designer.cs create mode 100644 src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.cs create mode 100644 src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.Designer.cs create mode 100644 src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.cs create mode 100644 src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.Designer.cs create mode 100644 src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.cs diff --git a/scripts/AddMigration.cmd b/scripts/AddMigration.cmd index 96133851..c73a3ed1 100644 --- a/scripts/AddMigration.cmd +++ b/scripts/AddMigration.cmd @@ -1 +1 @@ -dotnet ef migrations add --context TagzApp.Storage.Postgres.TagzAppContext -p ..\src\TagzApp.Storage.Postgres\ -s ..\src\TagzApp.Web %1 \ No newline at end of file +dotnet ef migrations add --context TagzApp.Storage.Postgres.TagzAppContext -p ..\src\TagzApp.Storage.Postgres\ -s ..\src\TagzApp.Blazor %1 \ No newline at end of file diff --git a/src/TagzApp.Common/IModerationRepository.cs b/src/TagzApp.Common/IModerationRepository.cs index a48d856a..08a72852 100644 --- a/src/TagzApp.Common/IModerationRepository.cs +++ b/src/TagzApp.Common/IModerationRepository.cs @@ -52,7 +52,7 @@ public interface IModerationRepository /// The moderator who is blocking the user /// The date the block expires /// - Task BlockUser(string userId, string provider, string userName, DateTimeOffset expirationDate); + Task BlockUser(string userId, string provider, string userName, DateTimeOffset expirationDate, BlockedUserCapabilities capabilities); /// /// Unblock a user diff --git a/src/TagzApp.Common/Models/BlockedUser.cs b/src/TagzApp.Common/Models/BlockedUser.cs index d4b2ee69..a940373d 100644 --- a/src/TagzApp.Common/Models/BlockedUser.cs +++ b/src/TagzApp.Common/Models/BlockedUser.cs @@ -31,4 +31,14 @@ public class BlockedUser /// public DateTimeOffset? ExpirationDate { get; set; } + public BlockedUserCapabilities Capabilities { get; set; } = BlockedUserCapabilities.Moderated; + +} + +public enum BlockedUserCapabilities +{ + + Moderated = 1, + Hidden = 2 + } diff --git a/src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.Designer.cs b/src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.Designer.cs new file mode 100644 index 00000000..af985f2b --- /dev/null +++ b/src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.Designer.cs @@ -0,0 +1,194 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TagzApp.Storage.Postgres; + +#nullable disable + +namespace TagzApp.Storage.Postgres.Migrations +{ + [DbContext(typeof(TagzAppContext))] + [Migration("20240625164553_AddBlockedUserCapabilities")] + partial class AddBlockedUserCapabilities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgBlockedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BlockDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("BlockingUser") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Capabilities") + .HasColumnType("integer"); + + b.Property("ExpirationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("BlockedUsers"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Emotes") + .HasColumnType("text"); + + b.Property("HashtagSought") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PreviewCard") + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProviderId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceUri") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "ProviderId"); + + b.ToTable("Content"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgModerationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint"); + + b.Property("Moderator") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProviderId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Reason") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "ProviderId"); + + b.HasIndex("ContentId") + .IsUnique(); + + b.ToTable("ModerationActions"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.Tag", b => + { + b.Property("Text") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Text"); + + b.ToTable("TagsWatched"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgModerationAction", b => + { + b.HasOne("TagzApp.Storage.Postgres.PgContent", "Content") + .WithOne("ModerationAction") + .HasForeignKey("TagzApp.Storage.Postgres.PgModerationAction", "ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgContent", b => + { + b.Navigation("ModerationAction"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.cs b/src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.cs new file mode 100644 index 00000000..64fa9d87 --- /dev/null +++ b/src/TagzApp.Storage.Postgres/Migrations/20240625164553_AddBlockedUserCapabilities.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TagzApp.Storage.Postgres.Migrations +{ + /// + public partial class AddBlockedUserCapabilities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Capabilities", + table: "BlockedUsers", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Capabilities", + table: "BlockedUsers"); + } + } +} diff --git a/src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.Designer.cs b/src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.Designer.cs new file mode 100644 index 00000000..d9aa60b1 --- /dev/null +++ b/src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.Designer.cs @@ -0,0 +1,198 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TagzApp.Storage.Postgres; + +#nullable disable + +namespace TagzApp.Storage.Postgres.Migrations +{ + [DbContext(typeof(TagzAppContext))] + [Migration("20240625165050_BlockedUserCapabilitiesDefaults")] + partial class BlockedUserCapabilitiesDefaults + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgBlockedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BlockDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("BlockingUser") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Capabilities") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ExpirationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "UserName"); + + b.ToTable("BlockedUsers"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Emotes") + .HasColumnType("text"); + + b.Property("HashtagSought") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PreviewCard") + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProviderId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceUri") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "ProviderId"); + + b.ToTable("Content"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgModerationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint"); + + b.Property("Moderator") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProviderId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Reason") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "ProviderId"); + + b.HasIndex("ContentId") + .IsUnique(); + + b.ToTable("ModerationActions"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.Tag", b => + { + b.Property("Text") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Text"); + + b.ToTable("TagsWatched"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgModerationAction", b => + { + b.HasOne("TagzApp.Storage.Postgres.PgContent", "Content") + .WithOne("ModerationAction") + .HasForeignKey("TagzApp.Storage.Postgres.PgModerationAction", "ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgContent", b => + { + b.Navigation("ModerationAction"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.cs b/src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.cs new file mode 100644 index 00000000..b274fd17 --- /dev/null +++ b/src/TagzApp.Storage.Postgres/Migrations/20240625165050_BlockedUserCapabilitiesDefaults.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TagzApp.Storage.Postgres.Migrations +{ + /// + public partial class BlockedUserCapabilitiesDefaults : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Capabilities", + table: "BlockedUsers", + type: "integer", + nullable: false, + defaultValue: 1, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddUniqueConstraint( + name: "AK_BlockedUsers_Provider_UserName", + table: "BlockedUsers", + columns: new[] { "Provider", "UserName" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "AK_BlockedUsers_Provider_UserName", + table: "BlockedUsers"); + + migrationBuilder.AlterColumn( + name: "Capabilities", + table: "BlockedUsers", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldDefaultValue: 1); + } + } +} diff --git a/src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.Designer.cs b/src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.Designer.cs new file mode 100644 index 00000000..e585cccd --- /dev/null +++ b/src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.Designer.cs @@ -0,0 +1,196 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TagzApp.Storage.Postgres; + +#nullable disable + +namespace TagzApp.Storage.Postgres.Migrations +{ + [DbContext(typeof(TagzAppContext))] + [Migration("20240625165405_RemovedBlockedUserAlternateKey")] + partial class RemovedBlockedUserAlternateKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgBlockedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BlockDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("BlockingUser") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Capabilities") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ExpirationDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("BlockedUsers"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Emotes") + .HasColumnType("text"); + + b.Property("HashtagSought") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PreviewCard") + .HasColumnType("text"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProviderId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceUri") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "ProviderId"); + + b.ToTable("Content"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgModerationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint"); + + b.Property("Moderator") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ProviderId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Reason") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasAlternateKey("Provider", "ProviderId"); + + b.HasIndex("ContentId") + .IsUnique(); + + b.ToTable("ModerationActions"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.Tag", b => + { + b.Property("Text") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Text"); + + b.ToTable("TagsWatched"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgModerationAction", b => + { + b.HasOne("TagzApp.Storage.Postgres.PgContent", "Content") + .WithOne("ModerationAction") + .HasForeignKey("TagzApp.Storage.Postgres.PgModerationAction", "ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TagzApp.Storage.Postgres.PgContent", b => + { + b.Navigation("ModerationAction"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.cs b/src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.cs new file mode 100644 index 00000000..29876f55 --- /dev/null +++ b/src/TagzApp.Storage.Postgres/Migrations/20240625165405_RemovedBlockedUserAlternateKey.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TagzApp.Storage.Postgres.Migrations +{ + /// + public partial class RemovedBlockedUserAlternateKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "AK_BlockedUsers_Provider_UserName", + table: "BlockedUsers"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddUniqueConstraint( + name: "AK_BlockedUsers_Provider_UserName", + table: "BlockedUsers", + columns: new[] { "Provider", "UserName" }); + } + } +} diff --git a/src/TagzApp.Storage.Postgres/Migrations/TagzAppContextModelSnapshot.cs b/src/TagzApp.Storage.Postgres/Migrations/TagzAppContextModelSnapshot.cs index dd7acbaa..0e7c3c75 100644 --- a/src/TagzApp.Storage.Postgres/Migrations/TagzAppContextModelSnapshot.cs +++ b/src/TagzApp.Storage.Postgres/Migrations/TagzAppContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -38,6 +38,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("Capabilities") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + b.Property("ExpirationDateTime") .HasColumnType("timestamp with time zone"); @@ -98,7 +103,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("Timestamp") + b.Property("Timestamp") .HasColumnType("timestamp with time zone"); b.Property("Type") diff --git a/src/TagzApp.Storage.Postgres/PgBlockedUser.cs b/src/TagzApp.Storage.Postgres/PgBlockedUser.cs index 35b9703f..49bf4405 100644 --- a/src/TagzApp.Storage.Postgres/PgBlockedUser.cs +++ b/src/TagzApp.Storage.Postgres/PgBlockedUser.cs @@ -22,6 +22,8 @@ public class PgBlockedUser public DateTimeOffset ExpirationDateTime { get; set; } = new DateTimeOffset(new DateTime(2050, 1, 1)); + public BlockedUserCapabilities Capabilities { get; set; } = BlockedUserCapabilities.Moderated; + // Add an explicit operator for converting between a blocked user and a PGblockeduser public static implicit operator BlockedUser(PgBlockedUser thisBlockedUser) { @@ -33,7 +35,8 @@ public static implicit operator BlockedUser(PgBlockedUser thisBlockedUser) UserName = thisBlockedUser.UserName, BlockingUser = thisBlockedUser.BlockingUser, BlockedDate = thisBlockedUser.BlockDateTime, - ExpirationDate = thisBlockedUser.ExpirationDateTime + ExpirationDate = thisBlockedUser.ExpirationDateTime, + Capabilities = thisBlockedUser.Capabilities }; diff --git a/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs b/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs index f9cf1296..30f5a134 100644 --- a/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs +++ b/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs @@ -160,7 +160,7 @@ public async Task GetCurrentBlockedUserCount() } - public async Task BlockUser(string userId, string provider, string userName, DateTimeOffset expirationDate) + public async Task BlockUser(string userId, string provider, string userName, DateTimeOffset expirationDate, BlockedUserCapabilities capabilities) { // add a new blocked user to the context @@ -169,7 +169,8 @@ public async Task BlockUser(string userId, string provider, string userName, Dat BlockingUser = userName, Provider = provider, UserName = userId, - ExpirationDateTime = expirationDate + ExpirationDateTime = expirationDate, + Capabilities = capabilities }; _Context.BlockedUsers.Add(blockedUser); diff --git a/src/TagzApp.Storage.Postgres/TagzApp.Storage.Postgres.csproj b/src/TagzApp.Storage.Postgres/TagzApp.Storage.Postgres.csproj index ae66f950..d494c66f 100644 --- a/src/TagzApp.Storage.Postgres/TagzApp.Storage.Postgres.csproj +++ b/src/TagzApp.Storage.Postgres/TagzApp.Storage.Postgres.csproj @@ -9,6 +9,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/src/TagzApp.Storage.Postgres/TagzAppContext.cs b/src/TagzApp.Storage.Postgres/TagzAppContext.cs index 0824321d..8fe59e66 100644 --- a/src/TagzApp.Storage.Postgres/TagzAppContext.cs +++ b/src/TagzApp.Storage.Postgres/TagzAppContext.cs @@ -4,8 +4,15 @@ namespace TagzApp.Storage.Postgres; public class TagzAppContext : DbContext { - public TagzAppContext(DbContextOptions options) : base(options) + + //public TagzAppContext(DbContextOptions options) : base(options) + //{ + //} + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + optionsBuilder.UseNpgsql(); + base.OnConfiguring(optionsBuilder); } public DbSet Content { get; set; } @@ -21,6 +28,10 @@ public TagzAppContext(DbContextOptions options) : base(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .Property(b => b.Capabilities) + .HasDefaultValue(BlockedUserCapabilities.Moderated); + modelBuilder.Entity().HasAlternateKey(c => new { c.Provider, c.ProviderId }); modelBuilder.Entity().HasOne(c => c.ModerationAction).WithOne(m => m.Content).HasForeignKey(m => m.ContentId); modelBuilder.Entity().Property(c => c.Timestamp) From 7d16ce1fb61a2ef3001812bfdbb1b6095f8dc2a0 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Thu, 27 Jun 2024 10:47:28 -0400 Subject: [PATCH 3/3] First pass at adding BlockedUserCapabilities --- .../Components/Admin/Pages/BlockedUsers.razor | 29 +++++++++++++++---- .../Components/Pages/MessageDetails.razor | 20 ++++++++++--- src/TagzApp.Blazor/Hubs/ModerationHub.cs | 8 ++++- .../PostgresModerationRepository.cs | 9 ++++-- .../SafetyModeration/AzureSafetyModeration.cs | 29 ++++++++++--------- .../TagzAppContext.cs | 14 ++++----- 6 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/TagzApp.Blazor/Components/Admin/Pages/BlockedUsers.razor b/src/TagzApp.Blazor/Components/Admin/Pages/BlockedUsers.razor index 44de7337..5506400e 100644 --- a/src/TagzApp.Blazor/Components/Admin/Pages/BlockedUsers.razor +++ b/src/TagzApp.Blazor/Components/Admin/Pages/BlockedUsers.razor @@ -5,6 +5,7 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.EntityFrameworkCore @using System.ComponentModel.DataAnnotations +@using TagzApp.ViewModels.Data @inject IMessagingService _Service @inject UserManager _UserManager @inject IModerationRepository _Repository @@ -17,10 +18,19 @@ @foreach (var provider in Providers) { - + } + + @* add a select to choose the BlockedUserCapabilities *@ + + @@ -35,6 +45,7 @@ Provider Username Blocked By + Capabilities Blocked On Blocked Until @@ -54,6 +65,7 @@ @user.Provider @@@(user.UserName.TrimStart('@')) @user.BlockingUser + @user.Capabilities.ToString() @user.BlockedDate.ToLocalTime().ToString("d") @(user.ExpirationDate > DateTime.Now.AddYears(1) ? "No end date" : user.ExpirationDate.Value.Date.ToLocalTime().ToString("d")) @@ -68,7 +80,7 @@ else @code { - private IEnumerable Providers { get; set; } + private AvailableProvider[] Providers { get; set; } = []; private IEnumerable Users { get; set; } [CascadingParameter] @@ -80,15 +92,21 @@ else [SupplyParameterFromForm(FormName = "BlockNewUser", Name = "username")] private string UserToBlock { get; set; } = string.Empty; + [SupplyParameterFromForm(FormName = "BlockNewUser", Name = "blockedUserCapabilities")] + private BlockedUserCapabilities? BlockedUserCapabilities { get; set; } + private object EditModel = new(); protected override async Task OnInitializedAsync() { - Providers = _Service.Providers - .OrderBy(x => x.DisplayName).ToArray(); Users = await _Repository.GetBlockedUsers(); + BlockedUserCapabilities ??= Common.Models.BlockedUserCapabilities.Moderated; + + Providers = _Service.Providers.Where(p => p.Enabled) + .Select(p => new AvailableProvider(p.Id, p.DisplayName)) + .ToArray(); } @@ -100,13 +118,14 @@ else var user = await _UserManager.FindByNameAsync(HttpContext.User.Identity.Name); if (user != null) { - await _Repository.BlockUser($"@{UserToBlock.TrimStart('@')}", SelectedProvider, user.DisplayName, new DateTimeOffset(new DateTime(2050, 1, 1), TimeSpan.Zero)); + await _Repository.BlockUser($"@{UserToBlock.TrimStart('@')}", SelectedProvider, user.DisplayName, new DateTimeOffset(new DateTime(2050, 1, 1), TimeSpan.Zero), this.BlockedUserCapabilities ?? Common.Models.BlockedUserCapabilities.Moderated ); } // reload the users list Users = await _Repository.GetBlockedUsers(); SelectedProvider = string.Empty; UserToBlock = string.Empty; + BlockedUserCapabilities = Common.Models.BlockedUserCapabilities.Moderated; } diff --git a/src/TagzApp.Blazor/Components/Pages/MessageDetails.razor b/src/TagzApp.Blazor/Components/Pages/MessageDetails.razor index 46ed8fef..3ebec042 100644 --- a/src/TagzApp.Blazor/Components/Pages/MessageDetails.razor +++ b/src/TagzApp.Blazor/Components/Pages/MessageDetails.razor @@ -95,10 +95,15 @@ @* Add a block user button *@
  • Block @Model.AuthorDisplayName on @Model.Provider.ToLowerInvariant().Humanize(LetterCasing.Title) - - @* TODO: Wire up the GO button - *@ + with @* Add an select for the enum BlockedUserCapabilities *@ + +
  • @@ -124,6 +129,8 @@ private string ValidationMessage { get; set; } + private BlockedUserCapabilities BlockedUserCapabilities { get; set; } = BlockedUserCapabilities.Moderated; + protected override async Task OnInitializedAsync() { @@ -139,7 +146,12 @@ var user = await UserManager.FindByNameAsync(HttpContext.User.Identity.Name); - await ModerationRepository.BlockUser(Model.AuthorUserName, Model.Provider, user.DisplayName, new DateTimeOffset(new DateTime(2050, 1, 1), TimeSpan.Zero)); + await ModerationRepository.BlockUser( + Model.AuthorUserName, + Model.Provider, + user.DisplayName, + new DateTimeOffset(new DateTime(2050, 1, 1), TimeSpan.Zero), + BlockedUserCapabilities); ValidationMessage = $"User {Model.AuthorUserName} has been blocked on {Model.Provider.ToLowerInvariant().Humanize(LetterCasing.Title)}"; } diff --git a/src/TagzApp.Blazor/Hubs/ModerationHub.cs b/src/TagzApp.Blazor/Hubs/ModerationHub.cs index b03f5245..e5cb9818 100644 --- a/src/TagzApp.Blazor/Hubs/ModerationHub.cs +++ b/src/TagzApp.Blazor/Hubs/ModerationHub.cs @@ -118,7 +118,13 @@ public async Task> GetFilteredContentByTag(s [ModerationState.Pending, ModerationState.Approved, ModerationState.Rejected] : new[] { Enum.Parse(state) }; - var results = (await _Service.GetFilteredContentByTag(tag, providers, states)) + var hiddenUsers = (await _Repository.GetBlockedUsers()) + .Where(b => b.Capabilities == BlockedUserCapabilities.Hidden) + .ToArray(); + + var results = (await _Service.GetFilteredContentByTag(tag, providers, states, 200)) + .Where(c => !hiddenUsers.Any(h => h.Provider.Equals(c.Item1.Provider, StringComparison.InvariantCultureIgnoreCase) && h.UserName.Equals(c.Item1.Author.UserName, StringComparison.InvariantCultureIgnoreCase))) + .Take(100) .Select(c => ModerationContentModel.ToModerationContentModel(c.Item1, c.Item2)) .ToArray(); Console.WriteLine($"Found {results.Length} results for {tag} with {providers.Length} providers and {states.Length} states"); diff --git a/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs b/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs index 30f5a134..85331576 100644 --- a/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs +++ b/src/TagzApp.Storage.Postgres/PostgresModerationRepository.cs @@ -180,7 +180,7 @@ public async Task BlockUser(string userId, string provider, string userName, Dat await _Context.SaveChangesAsync(); var blockedUsers = await GetBlockedUsers(); - _Cache.Set(KEY_BLOCKEDUSERS_CACHE, blockedUsers.Select(u => (u.Provider, u.UserName)).ToList()); + UpdateBlockedUsersCache(blockedUsers); } @@ -203,10 +203,15 @@ public async Task UnblockUser(string userId, string provider) await _Context.SaveChangesAsync(); var blockedUsers = await GetBlockedUsers(); - _Cache.Set(KEY_BLOCKEDUSERS_CACHE, blockedUsers.Select(u => (u.Provider, u.UserName)).ToList()); + UpdateBlockedUsersCache(blockedUsers); } + internal void UpdateBlockedUsersCache(IEnumerable blockedUsers) + { + _Cache.Set(KEY_BLOCKEDUSERS_CACHE, blockedUsers.Select(u => (u.Provider, u.UserName, u.Capabilities)).ToList()); + } + public async Task<(Content Content, ModerationAction Action)> GetContentWithModeration(string provider, string providerId) { diff --git a/src/TagzApp.Storage.Postgres/SafetyModeration/AzureSafetyModeration.cs b/src/TagzApp.Storage.Postgres/SafetyModeration/AzureSafetyModeration.cs index a155b3be..08039808 100644 --- a/src/TagzApp.Storage.Postgres/SafetyModeration/AzureSafetyModeration.cs +++ b/src/TagzApp.Storage.Postgres/SafetyModeration/AzureSafetyModeration.cs @@ -65,27 +65,30 @@ public void NotifyNewContent(string hashtag, Content content) // TODO: Establish a notification pipeline that allows for multiple moderation providers // Check if this content is created by one of the blocked users listed in the cache - var usersBlocked = _Cache.GetOrCreate(KEY_BLOCKEDUSERS_CACHE, _ => new List<(string Provider, string UserName)>()); + var usersBlocked = _Cache.GetOrCreate(KEY_BLOCKEDUSERS_CACHE, _ => new List<(string Provider, string UserName, BlockedUserCapabilities Capabilities)>()); var isBlocked = usersBlocked - .Any(a => a.Provider.Equals(content.Provider, StringComparison.InvariantCultureIgnoreCase) + .FirstOrDefault(a => a.Provider.Equals(content.Provider, StringComparison.InvariantCultureIgnoreCase) && a.UserName.Equals('@' + content.Author.UserName.TrimStart('@'), StringComparison.InvariantCultureIgnoreCase)); - if (isBlocked) + if (!string.IsNullOrEmpty(isBlocked.Provider)) { using var scope = _ServiceProvider.CreateScope(); var moderationRepository = scope.ServiceProvider.GetRequiredService(); moderationRepository.ModerateWithReason("BLOCKED-USER", content.Provider, content.ProviderId, ModerationState.Rejected, "Blocked User").GetAwaiter().GetResult(); - _NotifyNewMessages.NotifyNewContent(hashtag, content); - _NotifyNewMessages.NotifyRejectedContent(hashtag, content, new ModerationAction + if (isBlocked.Capabilities != BlockedUserCapabilities.Hidden) { - Provider = content.Provider, - ProviderId = content.ProviderId, - State = ModerationState.Rejected, - Timestamp = DateTimeOffset.UtcNow, - Moderator = "BLOCKED-USER", - Reason = "Blocked User" - }); + _NotifyNewMessages.NotifyNewContent(hashtag, content); + _NotifyNewMessages.NotifyRejectedContent(hashtag, content, new ModerationAction + { + Provider = content.Provider, + ProviderId = content.ProviderId, + State = ModerationState.Rejected, + Timestamp = DateTimeOffset.UtcNow, + Moderator = "BLOCKED-USER", + Reason = "Blocked User" + }); + } return; } @@ -174,7 +177,7 @@ private IServiceScope ReloadBlockedUserCache() var moderationRepository = scope.ServiceProvider.GetRequiredService(); var blockedUsers = moderationRepository.GetBlockedUsers().GetAwaiter().GetResult(); _AzureSafetyLogger.LogInformation($"Blocked user count: {blockedUsers.Count()}"); - _Cache.Set(KEY_BLOCKEDUSERS_CACHE, blockedUsers.Select(u => (u.Provider, u.UserName)).ToList()); + (moderationRepository as PostgresModerationRepository)?.UpdateBlockedUsersCache(blockedUsers); return scope; } diff --git a/src/TagzApp.Storage.Postgres/TagzAppContext.cs b/src/TagzApp.Storage.Postgres/TagzAppContext.cs index 8fe59e66..d64eac63 100644 --- a/src/TagzApp.Storage.Postgres/TagzAppContext.cs +++ b/src/TagzApp.Storage.Postgres/TagzAppContext.cs @@ -5,16 +5,16 @@ namespace TagzApp.Storage.Postgres; public class TagzAppContext : DbContext { - //public TagzAppContext(DbContextOptions options) : base(options) - //{ - //} - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + public TagzAppContext(DbContextOptions options) : base(options) { - optionsBuilder.UseNpgsql(); - base.OnConfiguring(optionsBuilder); } + //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + //{ + // optionsBuilder.UseNpgsql(); + // base.OnConfiguring(optionsBuilder); + //} + public DbSet Content { get; set; } public DbSet ModerationActions { get; set; }