From 81261fae49bf68d1d783077cd8b0b9173fd56065 Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 16 Sep 2025 09:52:15 -0500 Subject: [PATCH 01/40] Database changes for syncshell changes --- .../LightlessSyncShared/Data/MareDbContext.cs | 9 + ...45052_AddGroupProfilesAndDates.Designer.cs | 1149 +++++++++++++++++ ...20250916145052_AddGroupProfilesAndDates.cs | 80 ++ .../LightlessDbContextModelSnapshot.cs | 51 + .../LightlessSyncShared/Models/Group.cs | 1 + .../LightlessSyncShared/Models/GroupPair.cs | 2 + .../Models/GroupProfile.cs | 15 + 7 files changed, 1307 insertions(+) create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs diff --git a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs index dc7b17f..b9ece6c 100644 --- a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs +++ b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs @@ -53,6 +53,7 @@ public class LightlessDbContext : DbContext public DbSet CharaDataOriginalFiles { get; set; } public DbSet CharaDataPoses { get; set; } public DbSet CharaDataAllowances { get; set; } + public DbSet GroupProfiles { get; set; } protected override void OnModelCreating(ModelBuilder mb) { @@ -78,6 +79,14 @@ public class LightlessDbContext : DbContext mb.Entity().HasKey(u => new { u.GroupGID, u.BannedUserUID }); mb.Entity().HasIndex(c => c.BannedUserUID); mb.Entity().HasIndex(c => c.GroupGID); + mb.Entity().ToTable("group_profiles"); + mb.Entity().HasKey(u => u.GroupGID); + mb.Entity().HasIndex(c => c.GroupGID); + mb.Entity() + .HasOne(gp => gp.Group) + .WithMany() + .HasForeignKey(gp => gp.GroupGID) + .OnDelete(DeleteBehavior.Cascade); mb.Entity().ToTable("group_temp_invites"); mb.Entity().HasKey(u => new { u.GroupGID, u.Invite }); mb.Entity().HasIndex(c => c.GroupGID); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs new file mode 100644 index 0000000..7fc7446 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs @@ -0,0 +1,1149 @@ +// +using System; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20250916145052_AddGroupProfilesAndDates")] + partial class AddGroupProfilesAndDates + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("MarkForBan") + .HasColumnType("boolean") + .HasColumnName("mark_for_ban"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("BannedUid") + .HasColumnType("text") + .HasColumnName("banned_uid"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedGroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedGroupGID") + .HasDatabaseName("ix_chara_data_allowance_allowed_group_gid"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("RawSize") + .HasColumnType("bigint") + .HasColumnName("raw_size"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.Property("PreferDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_animations"); + + b.Property("PreferDisableSounds") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_sounds"); + + b.Property("PreferDisableVFX") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_vfx"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("FromFinder") + .HasColumnType("boolean") + .HasColumnName("from_finder"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.Property("JoinedGroupOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_group_on"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("UserUID", "GroupGID") + .HasName("pk_group_pair_preferred_permissions"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pair_preferred_permissions_group_gid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_group_pair_preferred_permissions_user_uid"); + + b.ToTable("group_pair_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Base64GroupProfileImage") + .HasColumnType("text") + .HasColumnName("base64group_profile_image"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.HasKey("GroupGID") + .HasName("pk_group_profiles"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_profiles_group_gid"); + + b.ToTable("group_profiles", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("DisableGroupAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_group_animations"); + + b.Property("DisableGroupSounds") + .HasColumnType("boolean") + .HasColumnName("disable_group_sounds"); + + b.Property("DisableGroupVFX") + .HasColumnType("boolean") + .HasColumnName("disable_group_vfx"); + + b.Property("DisableIndividualAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_individual_animations"); + + b.Property("DisableIndividualSounds") + .HasColumnType("boolean") + .HasColumnName("disable_individual_sounds"); + + b.Property("DisableIndividualVFX") + .HasColumnType("boolean") + .HasColumnName("disable_individual_vfx"); + + b.Property("IndividualIsSticky") + .HasColumnType("boolean") + .HasColumnName("individual_is_sticky"); + + b.HasKey("UserUID") + .HasName("pk_user_default_preferred_permissions"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_default_preferred_permissions_user_uid"); + + b.ToTable("user_default_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Sticky") + .HasColumnType("boolean") + .HasColumnName("sticky"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_user_permission_sets"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_user_permission_sets_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_permission_sets_user_uid"); + + b.HasIndex("UserUID", "OtherUserUID", "IsPaused") + .HasDatabaseName("ix_user_permission_sets_user_uid_other_user_uid_is_paused"); + + b.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedGroup"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_uid"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_profiles_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs new file mode 100644 index 0000000..00d7d92 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs @@ -0,0 +1,80 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AddGroupProfilesAndDates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "created_date", + table: "groups", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "from_finder", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "joined_group_on", + table: "group_pairs", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateTable( + name: "group_profiles", + columns: table => new + { + group_gid = table.Column(type: "character varying(20)", nullable: false), + description = table.Column(type: "text", nullable: true), + tags = table.Column(type: "text", nullable: true), + base64group_profile_image = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_group_profiles", x => x.group_gid); + table.ForeignKey( + name: "fk_group_profiles_groups_group_gid", + column: x => x.group_gid, + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_group_profiles_group_gid", + table: "group_profiles", + column: "group_gid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_profiles"); + + migrationBuilder.DropColumn( + name: "created_date", + table: "groups"); + + migrationBuilder.DropColumn( + name: "from_finder", + table: "group_pairs"); + + migrationBuilder.DropColumn( + name: "joined_group_on", + table: "group_pairs"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index e2ce4c6..cfc8535 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -430,6 +430,10 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(50)") .HasColumnName("alias"); + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + b.Property("HashedPassword") .HasColumnType("text") .HasColumnName("hashed_password"); @@ -510,6 +514,10 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(10)") .HasColumnName("group_user_uid"); + b.Property("FromFinder") + .HasColumnType("boolean") + .HasColumnName("from_finder"); + b.Property("IsModerator") .HasColumnType("boolean") .HasColumnName("is_moderator"); @@ -518,6 +526,10 @@ namespace LightlessSyncServer.Migrations .HasColumnType("boolean") .HasColumnName("is_pinned"); + b.Property("JoinedGroupOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_group_on"); + b.HasKey("GroupGID", "GroupUserUID") .HasName("pk_group_pairs"); @@ -568,6 +580,33 @@ namespace LightlessSyncServer.Migrations b.ToTable("group_pair_preferred_permissions", (string)null); }); + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Base64GroupProfileImage") + .HasColumnType("text") + .HasColumnName("base64group_profile_image"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.HasKey("GroupGID") + .HasName("pk_group_profiles"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_profiles_group_gid"); + + b.ToTable("group_profiles", (string)null); + }); + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => { b.Property("GroupGID") @@ -1010,6 +1049,18 @@ namespace LightlessSyncServer.Migrations b.Navigation("User"); }); + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_profiles_groups_group_gid"); + + b.Navigation("Group"); + }); + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => { b.HasOne("LightlessSyncShared.Models.Group", "Group") diff --git a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs index 67de63d..2188a5b 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs @@ -16,4 +16,5 @@ public class Group public bool PreferDisableSounds { get; set; } public bool PreferDisableAnimations { get; set; } public bool PreferDisableVFX { get; set; } + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs index bb5824e..15e0e7f 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs @@ -8,4 +8,6 @@ public class GroupPair public User GroupUser { get; set; } public bool IsPinned { get; set; } public bool IsModerator { get; set; } + public bool FromFinder { get; set; } = false; + public DateTime JoinedGroupOn { get; set; } } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs new file mode 100644 index 0000000..ea750d8 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LightlessSyncShared.Models; +public class GroupProfile +{ + public string GroupGID { get; set; } + public Group Group { get; set; } + public string Description { get; set; } + public string Tags { get; set; } + public string Base64GroupProfileImage { get; set; } +} From 0df7ee424d820e208c34c80a01d685a21ec3182a Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 16 Sep 2025 15:03:15 -0500 Subject: [PATCH 02/40] db changes --- .../LightlessSyncShared/Data/MareDbContext.cs | 3 +++ ...> 20250916200240_AddGroupProfilesAndDates.Designer.cs} | 8 +++++--- ...ates.cs => 20250916200240_AddGroupProfilesAndDates.cs} | 5 ++--- .../Migrations/LightlessDbContextModelSnapshot.cs | 6 ++++-- .../LightlessSyncShared/Models/GroupPair.cs | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) rename LightlessSyncServer/LightlessSyncShared/Migrations/{20250916145052_AddGroupProfilesAndDates.Designer.cs => 20250916200240_AddGroupProfilesAndDates.Designer.cs} (99%) rename LightlessSyncServer/LightlessSyncShared/Migrations/{20250916145052_AddGroupProfilesAndDates.cs => 20250916200240_AddGroupProfilesAndDates.cs} (92%) diff --git a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs index b9ece6c..4f38ad8 100644 --- a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs +++ b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs @@ -71,6 +71,9 @@ public class LightlessDbContext : DbContext mb.Entity().ToTable("banned_registrations"); mb.Entity().ToTable("groups"); mb.Entity().HasIndex(c => c.OwnerUID); + mb.Entity() + .Property(g => g.CreatedDate) + .HasDefaultValueSql("CURRENT_TIMESTAMP"); mb.Entity().ToTable("group_pairs"); mb.Entity().HasKey(u => new { u.GroupGID, u.GroupUserUID }); mb.Entity().HasIndex(c => c.GroupUserUID); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916200240_AddGroupProfilesAndDates.Designer.cs similarity index 99% rename from LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs rename to LightlessSyncServer/LightlessSyncShared/Migrations/20250916200240_AddGroupProfilesAndDates.Designer.cs index 7fc7446..0ad25ec 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.Designer.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916200240_AddGroupProfilesAndDates.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LightlessSyncServer.Migrations { [DbContext(typeof(LightlessDbContext))] - [Migration("20250916145052_AddGroupProfilesAndDates")] + [Migration("20250916200240_AddGroupProfilesAndDates")] partial class AddGroupProfilesAndDates { /// @@ -434,8 +434,10 @@ namespace LightlessSyncServer.Migrations .HasColumnName("alias"); b.Property("CreatedDate") + .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") - .HasColumnName("created_date"); + .HasColumnName("created_date") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); b.Property("HashedPassword") .HasColumnType("text") @@ -529,7 +531,7 @@ namespace LightlessSyncServer.Migrations .HasColumnType("boolean") .HasColumnName("is_pinned"); - b.Property("JoinedGroupOn") + b.Property("JoinedGroupOn") .HasColumnType("timestamp with time zone") .HasColumnName("joined_group_on"); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916200240_AddGroupProfilesAndDates.cs similarity index 92% rename from LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs rename to LightlessSyncServer/LightlessSyncShared/Migrations/20250916200240_AddGroupProfilesAndDates.cs index 00d7d92..523ceee 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20250916145052_AddGroupProfilesAndDates.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250916200240_AddGroupProfilesAndDates.cs @@ -16,7 +16,7 @@ namespace LightlessSyncServer.Migrations table: "groups", type: "timestamp with time zone", nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + defaultValueSql: "CURRENT_TIMESTAMP"); migrationBuilder.AddColumn( name: "from_finder", @@ -29,8 +29,7 @@ namespace LightlessSyncServer.Migrations name: "joined_group_on", table: "group_pairs", type: "timestamp with time zone", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + nullable: true); migrationBuilder.CreateTable( name: "group_profiles", diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index cfc8535..391143b 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -431,8 +431,10 @@ namespace LightlessSyncServer.Migrations .HasColumnName("alias"); b.Property("CreatedDate") + .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") - .HasColumnName("created_date"); + .HasColumnName("created_date") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); b.Property("HashedPassword") .HasColumnType("text") @@ -526,7 +528,7 @@ namespace LightlessSyncServer.Migrations .HasColumnType("boolean") .HasColumnName("is_pinned"); - b.Property("JoinedGroupOn") + b.Property("JoinedGroupOn") .HasColumnType("timestamp with time zone") .HasColumnName("joined_group_on"); diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs index 15e0e7f..f04db3f 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs @@ -9,5 +9,5 @@ public class GroupPair public bool IsPinned { get; set; } public bool IsModerator { get; set; } public bool FromFinder { get; set; } = false; - public DateTime JoinedGroupOn { get; set; } + public DateTime? JoinedGroupOn { get; set; } } From f5b03846fee280cc40c5dc4a2e2b856d9c1b7c0c Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 17 Sep 2025 02:07:46 +0200 Subject: [PATCH 03/40] Added changes for the profiles to be returned and be able to be changed. --- .../Hubs/MareHub.Groups.cs | 121 ++++++++++++------ .../LightlessSyncServer/Utils/Extensions.cs | 2 +- .../LightlessSyncShared/Models/Group.cs | 1 + .../LightlessSyncShared/Models/GroupPair.cs | 1 + .../Models/GroupProfile.cs | 8 +- 5 files changed, 86 insertions(+), 47 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index f9895ff..f0468dd 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -57,7 +57,7 @@ public partial class LightlessHub group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations); group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX); - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList(); await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false); @@ -151,15 +151,15 @@ public partial class LightlessHub public async Task GroupCreate() { _logger.LogCallInfo(); - var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false); - var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false); + var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) { throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); } var gid = StringUtils.GenerateRandomString(12); - while (await DbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false)) + while (await DbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) { gid = StringUtils.GenerateRandomString(12); } @@ -168,8 +168,9 @@ public partial class LightlessHub var passwd = StringUtils.GenerateRandomString(16); using var sha = SHA256.Create(); var hashedPw = StringUtils.Sha256String(passwd); + var currentTime = DateTime.UtcNow; - UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false); + UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); Group newGroup = new() { @@ -179,7 +180,8 @@ public partial class LightlessHub OwnerUID = UserUID, PreferDisableAnimations = defaultPermissions.DisableGroupAnimations, PreferDisableSounds = defaultPermissions.DisableGroupSounds, - PreferDisableVFX = defaultPermissions.DisableGroupVFX + PreferDisableVFX = defaultPermissions.DisableGroupVFX, + CreatedDate = currentTime, }; GroupPair initialPair = new() @@ -187,6 +189,7 @@ public partial class LightlessHub GroupGID = newGroup.GID, GroupUserUID = UserUID, IsPinned = true, + JoinedGroupOn = currentTime, }; GroupPairPreferredPermission initialPrefPermissions = new() @@ -195,18 +198,18 @@ public partial class LightlessHub GroupGID = newGroup.GID, DisableSounds = defaultPermissions.DisableGroupSounds, DisableAnimations = defaultPermissions.DisableGroupAnimations, - DisableVFX = defaultPermissions.DisableGroupAnimations + DisableVFX = defaultPermissions.DisableGroupAnimations, }; - await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false); - await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false); - await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions).ConfigureAwait(false); - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.Groups.AddAsync(newGroup, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(initialPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), - newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal))) + newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), initialPair.JoinedGroupOn, new(StringComparer.Ordinal))) .ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(gid)); @@ -262,10 +265,10 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); - var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); DbContext.RemoveRange(groupPairs); DbContext.Remove(group); - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); @@ -278,9 +281,9 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false); - if (!userHasRights) return new List(); + if (!userHasRights) return []; - var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync().ConfigureAwait(false); + var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); List bannedGroupUsers = banEntries.Select(b => new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn, @@ -298,14 +301,14 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; - var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var hashedPw = StringUtils.Sha256String(dto.Password); - var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); - var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); - var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); - var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (group == null || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) @@ -357,6 +360,7 @@ public partial class LightlessHub { GroupGID = group.GID, GroupUserUID = UserUID, + JoinedGroupOn = DateTime.UtcNow, }; var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID).ConfigureAwait(false); @@ -369,7 +373,7 @@ public partial class LightlessHub DisableSounds = dto.GroupUserPreferredPermissions.IsDisableSounds(), DisableVFX = dto.GroupUserPreferredPermissions.IsDisableVFX(), DisableAnimations = dto.GroupUserPreferredPermissions.IsDisableAnimations(), - IsPaused = false + IsPaused = false, }; DbContext.Add(newPerms); @@ -384,15 +388,15 @@ public partial class LightlessHub DbContext.Update(preferredPermissions); } - await DbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(newPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "Success")); - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync().ConfigureAwait(false); + var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), - group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), + group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), newPair.JoinedGroupOn, groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false); var self = DbContext.Users.Single(u => u.UID == UserUID); @@ -518,7 +522,7 @@ public partial class LightlessHub } } - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); return true; } @@ -541,8 +545,8 @@ public partial class LightlessHub .Where(g => g.GroupGID == dto.Group.GID) .ToListAsync().ConfigureAwait(false); var usersToPrune = allGroupUsers.Where(p => !p.IsPinned && !p.IsModerator - && p.GroupUserUID != UserUID - && p.Group.OwnerUID != p.GroupUserUID + && !string.Equals(p.GroupUserUID, UserUID, StringComparison.Ordinal) + && !string.Equals(p.Group.OwnerUID, p.GroupUserUID, StringComparison.Ordinal) && p.GroupUser.LastLoggedIn.AddDays(days) < DateTime.UtcNow); if (!execute) return usersToPrune.Count(); @@ -600,6 +604,45 @@ public partial class LightlessHub } } + [Authorize(Policy = "Identified")] + public async Task GroupSetProfile(GroupProfileDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + if (dto.Group == null) return; + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return; + + var groupProfileDb = await DbContext.GroupProfiles + .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, + _contextAccessor.HttpContext.RequestAborted) + .ConfigureAwait(false); + + if (groupProfileDb != null) + { + groupProfileDb.Description = dto.Description; + groupProfileDb.Tags = dto.Tags; + groupProfileDb.Base64GroupProfileImage = dto.PictureBase64; + } + else + { + var groupProfile = new GroupProfile + { + GroupGID = dto.Group.GID, + Description = dto.Description, + Tags = dto.Tags, + Base64GroupProfileImage = dto.PictureBase64, + }; + + await DbContext.GroupProfiles.AddAsync(groupProfile, + _contextAccessor.HttpContext.RequestAborted) + .ConfigureAwait(false); + } + + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + } + [Authorize(Policy = "Identified")] public async Task GroupSetUserInfo(GroupPairUserInfoDto dto) { @@ -629,9 +672,9 @@ public partial class LightlessHub userPair.IsModerator = false; } - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false); } @@ -640,15 +683,15 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); - var preferredPermissions = (await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == UserUID).ToListAsync().ConfigureAwait(false)) + var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var preferredPermissions = (await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == UserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) .Where(u => groups.Exists(k => string.Equals(k.GroupGID, u.GroupGID, StringComparison.Ordinal))) .ToDictionary(u => groups.First(f => string.Equals(f.GroupGID, u.GroupGID, StringComparison.Ordinal)), u => u); var groupInfos = await DbContext.GroupPairs.Where(u => groups.Select(g => g.GroupGID).Contains(u.GroupGID) && (u.IsPinned || u.IsModerator)) - .ToListAsync().ConfigureAwait(false); + .ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); return preferredPermissions.Select(g => new GroupFullInfoDto(g.Key.Group.ToGroupData(), g.Key.Group.Owner.ToUserData(), - g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), + g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), g.Key.JoinedGroupOn, groupInfos.Where(i => string.Equals(i.GroupGID, g.Key.GroupGID, StringComparison.Ordinal)) .ToDictionary(i => i.GroupUserUID, i => i.ToEnum(), StringComparer.Ordinal))).ToList(); } @@ -661,11 +704,11 @@ public partial class LightlessHub var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); if (!userHasRights) return; - var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID).ConfigureAwait(false); + var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (banEntry == null) return; DbContext.Remove(banEntry); - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); } diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index 070ff9e..59d30ba 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -10,7 +10,7 @@ public static class Extensions { public static GroupData ToGroupData(this Group group) { - return new GroupData(group.GID, group.Alias); + return new GroupData(group.GID, group.Alias, group.CreatedDate, group.Profile.Description, group.Profile.Tags, group.Profile.Base64GroupProfileImage); } public static UserData ToUserData(this GroupPair pair) diff --git a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs index 2188a5b..4d6e26e 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs @@ -9,6 +9,7 @@ public class Group public string GID { get; set; } public string OwnerUID { get; set; } public User Owner { get; set; } + public GroupProfile? Profile { get; set; } [MaxLength(50)] public string Alias { get; set; } public bool InvitesEnabled { get; set; } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs index f04db3f..a0b7de8 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs @@ -4,6 +4,7 @@ public class GroupPair { public string GroupGID { get; set; } public Group Group { get; set; } + public GroupProfile Profile { get; set; } public string GroupUserUID { get; set; } public User GroupUser { get; set; } public bool IsPinned { get; set; } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs index ea750d8..347f79d 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LightlessSyncShared.Models; +namespace LightlessSyncShared.Models; public class GroupProfile { public string GroupGID { get; set; } From deea39d62177c1effba28d36f3aa9045261ea378 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 17 Sep 2025 02:38:20 +0200 Subject: [PATCH 04/40] Added get of group profile, removed group from model. redone group data. --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 12 ++++++++++++ .../LightlessSyncServer/Utils/Extensions.cs | 2 +- .../LightlessSyncShared/Models/Group.cs | 1 - 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index f0468dd..5241b0c 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -1,6 +1,7 @@ using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; using LightlessSyncShared.Models; using LightlessSyncShared.Utils; @@ -604,6 +605,17 @@ public partial class LightlessHub } } + [Authorize(Policy = "Identified")] + public async Task GroupGetProfile(GroupDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + var data = await DbContext.GroupProfiles.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + if (data == null) return new GroupProfileDto(dto.Group, null, null, null); + + return new GroupProfileDto(dto.Group, data.Description, data.Tags, data.Base64GroupProfileImage); + } + [Authorize(Policy = "Identified")] public async Task GroupSetProfile(GroupProfileDto dto) { diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index 59d30ba..34bedac 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -10,7 +10,7 @@ public static class Extensions { public static GroupData ToGroupData(this Group group) { - return new GroupData(group.GID, group.Alias, group.CreatedDate, group.Profile.Description, group.Profile.Tags, group.Profile.Base64GroupProfileImage); + return new GroupData(group.GID, group.Alias, group.CreatedDate); } public static UserData ToUserData(this GroupPair pair) diff --git a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs index 4d6e26e..2188a5b 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs @@ -9,7 +9,6 @@ public class Group public string GID { get; set; } public string OwnerUID { get; set; } public User Owner { get; set; } - public GroupProfile? Profile { get; set; } [MaxLength(50)] public string Alias { get; set; } public bool InvitesEnabled { get; set; } From b669e2cb24b91d2ce80e479cd45258558c123bd7 Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 16 Sep 2025 19:54:31 -0500 Subject: [PATCH 05/40] Adding group profile to the group model --- .../LightlessSyncShared/Data/MareDbContext.cs | 10 +- ...50917004805_AddProfilesToGroup.Designer.cs | 1155 +++++++++++++++++ .../20250917004805_AddProfilesToGroup.cs | 41 + .../LightlessDbContextModelSnapshot.cs | 12 +- .../LightlessSyncShared/Models/Group.cs | 1 + .../Models/GroupProfile.cs | 3 + 6 files changed, 1213 insertions(+), 9 deletions(-) create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.cs diff --git a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs index 4f38ad8..fe17706 100644 --- a/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs +++ b/LightlessSyncServer/LightlessSyncShared/Data/MareDbContext.cs @@ -74,6 +74,11 @@ public class LightlessDbContext : DbContext mb.Entity() .Property(g => g.CreatedDate) .HasDefaultValueSql("CURRENT_TIMESTAMP"); + mb.Entity() + .HasOne(g => g.Profile) + .WithOne(p => p.Group) + .HasForeignKey(p => p.GroupGID) + .IsRequired(false); mb.Entity().ToTable("group_pairs"); mb.Entity().HasKey(u => new { u.GroupGID, u.GroupUserUID }); mb.Entity().HasIndex(c => c.GroupUserUID); @@ -85,11 +90,6 @@ public class LightlessDbContext : DbContext mb.Entity().ToTable("group_profiles"); mb.Entity().HasKey(u => u.GroupGID); mb.Entity().HasIndex(c => c.GroupGID); - mb.Entity() - .HasOne(gp => gp.Group) - .WithMany() - .HasForeignKey(gp => gp.GroupGID) - .OnDelete(DeleteBehavior.Cascade); mb.Entity().ToTable("group_temp_invites"); mb.Entity().HasKey(u => new { u.GroupGID, u.Invite }); mb.Entity().HasIndex(c => c.GroupGID); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.Designer.cs new file mode 100644 index 0000000..95bcd9b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.Designer.cs @@ -0,0 +1,1155 @@ +// +using System; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20250917004805_AddProfilesToGroup")] + partial class AddProfilesToGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("MarkForBan") + .HasColumnType("boolean") + .HasColumnName("mark_for_ban"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("BannedUid") + .HasColumnType("text") + .HasColumnName("banned_uid"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedGroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedGroupGID") + .HasDatabaseName("ix_chara_data_allowance_allowed_group_gid"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("RawSize") + .HasColumnType("bigint") + .HasColumnName("raw_size"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.Property("PreferDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_animations"); + + b.Property("PreferDisableSounds") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_sounds"); + + b.Property("PreferDisableVFX") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_vfx"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("FromFinder") + .HasColumnType("boolean") + .HasColumnName("from_finder"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.Property("JoinedGroupOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_group_on"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("UserUID", "GroupGID") + .HasName("pk_group_pair_preferred_permissions"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pair_preferred_permissions_group_gid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_group_pair_preferred_permissions_user_uid"); + + b.ToTable("group_pair_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.Property("GroupGID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Base64GroupProfileImage") + .HasColumnType("text") + .HasColumnName("base64group_profile_image"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.HasKey("GroupGID") + .HasName("pk_group_profiles"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_profiles_group_gid"); + + b.ToTable("group_profiles", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("DisableGroupAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_group_animations"); + + b.Property("DisableGroupSounds") + .HasColumnType("boolean") + .HasColumnName("disable_group_sounds"); + + b.Property("DisableGroupVFX") + .HasColumnType("boolean") + .HasColumnName("disable_group_vfx"); + + b.Property("DisableIndividualAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_individual_animations"); + + b.Property("DisableIndividualSounds") + .HasColumnType("boolean") + .HasColumnName("disable_individual_sounds"); + + b.Property("DisableIndividualVFX") + .HasColumnType("boolean") + .HasColumnName("disable_individual_vfx"); + + b.Property("IndividualIsSticky") + .HasColumnType("boolean") + .HasColumnName("individual_is_sticky"); + + b.HasKey("UserUID") + .HasName("pk_user_default_preferred_permissions"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_default_preferred_permissions_user_uid"); + + b.ToTable("user_default_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Sticky") + .HasColumnType("boolean") + .HasColumnName("sticky"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_user_permission_sets"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_user_permission_sets_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_permission_sets_user_uid"); + + b.HasIndex("UserUID", "OtherUserUID", "IsPaused") + .HasDatabaseName("ix_user_permission_sets_user_uid_other_user_uid_is_paused"); + + b.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedGroup"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_uid"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithOne("Profile") + .HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID") + .HasConstraintName("fk_group_profiles_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Navigation("Profile"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.cs new file mode 100644 index 0000000..858430c --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250917004805_AddProfilesToGroup.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AddProfilesToGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_group_profiles_groups_group_gid", + table: "group_profiles"); + + migrationBuilder.AddForeignKey( + name: "fk_group_profiles_groups_group_gid", + table: "group_profiles", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_group_profiles_groups_group_gid", + table: "group_profiles"); + + migrationBuilder.AddForeignKey( + name: "fk_group_profiles_groups_group_gid", + table: "group_profiles", + column: "group_gid", + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index 391143b..01f6009 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -585,6 +585,7 @@ namespace LightlessSyncServer.Migrations modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => { b.Property("GroupGID") + .HasMaxLength(20) .HasColumnType("character varying(20)") .HasColumnName("group_gid"); @@ -1054,10 +1055,8 @@ namespace LightlessSyncServer.Migrations modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => { b.HasOne("LightlessSyncShared.Models.Group", "Group") - .WithMany() - .HasForeignKey("GroupGID") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() + .WithOne("Profile") + .HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID") .HasConstraintName("fk_group_profiles_groups_group_gid"); b.Navigation("Group"); @@ -1142,6 +1141,11 @@ namespace LightlessSyncServer.Migrations b.Navigation("Poses"); }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Navigation("Profile"); + }); #pragma warning restore 612, 618 } } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs index 2188a5b..522c7d8 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/Group.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/Group.cs @@ -11,6 +11,7 @@ public class Group public User Owner { get; set; } [MaxLength(50)] public string Alias { get; set; } + public GroupProfile? Profile { get; set; } public bool InvitesEnabled { get; set; } public string HashedPassword { get; set; } public bool PreferDisableSounds { get; set; } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs index ea750d8..fdc1717 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -7,6 +8,8 @@ using System.Threading.Tasks; namespace LightlessSyncShared.Models; public class GroupProfile { + [Key] + [MaxLength(20)] public string GroupGID { get; set; } public Group Group { get; set; } public string Description { get; set; } From 931ca0d62224fa36f3961cc8e021d67b53ea63fe Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 17 Sep 2025 05:19:37 +0200 Subject: [PATCH 06/40] Added parameters --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 5241b0c..9e5b8c2 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -609,11 +609,16 @@ public partial class LightlessHub public async Task GroupGetProfile(GroupDto dto) { _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - - var data = await DbContext.GroupProfiles.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - if (data == null) return new GroupProfileDto(dto.Group, null, null, null); - return new GroupProfileDto(dto.Group, data.Description, data.Tags, data.Base64GroupProfileImage); + var cancellationToken = _contextAccessor.HttpContext.RequestAborted; + + var data = await DbContext.GroupProfiles + .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken) + .ConfigureAwait(false); + + return data == null + ? new GroupProfileDto(dto.Group, null, null, null) + : new GroupProfileDto(dto.Group, data.Description, data.Tags, data.Base64GroupProfileImage); } [Authorize(Policy = "Identified")] From 6fb5f6e9a7f8171fda530dd4708ffb95cb8e907f Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 17 Sep 2025 05:37:04 +0200 Subject: [PATCH 07/40] Added client sendback of profileDTO --- LightlessAPI | 2 +- .../Hubs/MareHub.ClientStubs.cs | 19 +------- .../Hubs/MareHub.Groups.cs | 45 ++++++++++++------- .../LightlessSyncServer/Hubs/MareHub.User.cs | 20 ++++----- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 4ce70be..96744f4 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 4ce70bee8354d0c96d73e65312d39a826810dc60 +Subproject commit 96744f4f28142a5cd40103d6ee209761b354b818 diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs index d148f91..c4fbe0c 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs @@ -10,41 +10,24 @@ namespace LightlessSyncServer.Hubs public partial class LightlessHub { public Task Client_DownloadReady(Guid requestId) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_GroupDelete(GroupDto groupDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_GroupPairLeft(GroupPairDto groupPairDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - + public Task Client_GroupSendProfile(GroupProfileDto groupProfile) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_GroupSendInfo(GroupInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserAddClientPair(UserPairDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserReceiveUploadStatus(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserRemoveClientPair(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserSendOffline(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserSendOnline(OnlineUserIdentDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserUpdateProfile(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); - public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_UserUpdateDefaultPermissions(DefaultPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 9e5b8c2..24bb313 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -1,7 +1,6 @@ using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; -using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; using LightlessSyncShared.Models; using LightlessSyncShared.Utils; @@ -136,7 +135,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync().ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) @@ -330,14 +329,14 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; - var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var hashedPw = StringUtils.Sha256String(dto.Password); - var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); - var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); - var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); - var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (group == null || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) @@ -364,7 +363,7 @@ public partial class LightlessHub JoinedGroupOn = DateTime.UtcNow, }; - var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID).ConfigureAwait(false); + var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (preferredPermissions == null) { GroupPairPreferredPermission newPerms = new() @@ -560,7 +559,7 @@ public partial class LightlessHub .Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); } - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); return usersToPrune.Count(); } @@ -584,15 +583,15 @@ public partial class LightlessHub var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList(); await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync().ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); if (userIdent == null) { - await DbContext.SaveChangesAsync().ConfigureAwait(false); + await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); return; } @@ -616,9 +615,23 @@ public partial class LightlessHub .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken) .ConfigureAwait(false); - return data == null - ? new GroupProfileDto(dto.Group, null, null, null) - : new GroupProfileDto(dto.Group, data.Description, data.Tags, data.Base64GroupProfileImage); + var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null); + + if (data is not null) + { + profileDto = profileDto with + { + Description = data.Description, + Tags = data.Tags, + PictureBase64 = data.Base64GroupProfileImage, + }; + + await Clients.User(UserUID) + .Client_GroupSendProfile(profileDto) + .ConfigureAwait(false); + } + + return profileDto; } [Authorize(Policy = "Identified")] diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs index 53162a2..0ea7d14 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs @@ -30,7 +30,7 @@ public partial class LightlessHub if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return; // grab other user, check if it exists and if a pair already exists - var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false); + var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (otherUser == null) { await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false); @@ -46,7 +46,7 @@ public partial class LightlessHub var existingEntry = await DbContext.ClientPairs.AsNoTracking() .FirstOrDefaultAsync(p => - p.User.UID == UserUID && p.OtherUserUID == otherUser.UID).ConfigureAwait(false); + p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (existingEntry != null) { @@ -55,11 +55,11 @@ public partial class LightlessHub } // grab self create new client pair and save - var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); - ClientPair wl = new ClientPair() + ClientPair wl = new() { OtherUser = otherUser, User = user, @@ -71,7 +71,7 @@ public partial class LightlessHub var permissions = existingData?.OwnPermissions; if (permissions == null || !permissions.Sticky) { - var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID).ConfigureAwait(false); + var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); permissions = new UserPermissionSet() { @@ -84,7 +84,7 @@ public partial class LightlessHub Sticky = true }; - var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID).ConfigureAwait(false); + var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (existingDbPerms == null) { await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false); @@ -142,7 +142,7 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); foreach (var user in secondaryUsers) { @@ -195,7 +195,7 @@ public partial class LightlessHub return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); } - var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID).ConfigureAwait(false); + var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (data == null) return new UserProfileDto(user.User, false, null, null, null); if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation"); @@ -296,7 +296,7 @@ public partial class LightlessHub // check if client pair even exists ClientPair callerPair = - await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false); + await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (callerPair == null) return; var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false); @@ -347,7 +347,7 @@ public partial class LightlessHub if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); - var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false); + var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); if (existingData?.FlaggedForReport ?? false) { From 5b3fe6e240716a69a8e8e0df0f5d78eb9d3a49b1 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Wed, 24 Sep 2025 06:33:56 +0900 Subject: [PATCH 08/40] lightfinder! --- LightlessAPI | 2 +- .../Hubs/MareHub.Functions.cs | 28 +- .../Hubs/MareHub.Groups.cs | 161 ++++++++- .../LightlessSyncServer/Hubs/MareHub.User.cs | 328 +++++++++++++++++- .../LightlessSyncServer/Hubs/MareHub.cs | 4 +- .../Services/PairService.cs | 108 ++++++ .../LightlessSyncServer/Startup.cs | 1 + 7 files changed, 618 insertions(+), 14 deletions(-) create mode 100644 LightlessSyncServer/LightlessSyncServer/Services/PairService.cs diff --git a/LightlessAPI b/LightlessAPI index 4ce70be..fd4cd52 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 4ce70bee8354d0c96d73e65312d39a826810dc60 +Subproject commit fd4cd52d2e78c8a621e6b06149e69842bb5ff255 diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs index fafc432..6344f40 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs @@ -323,7 +323,9 @@ public partial class LightlessHub GID = user.Gid, Synced = user.Synced, OwnPermissions = ownperm, - OtherPermissions = otherperm + OtherPermissions = otherperm, + OtherUserIsAdmin = u.IsAdmin, + OtherUserIsModerator = u.IsModerator }; var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); @@ -336,7 +338,9 @@ public partial class LightlessHub resultList.Max(p => p.Synced), resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), resultList[0].OwnPermissions, - resultList[0].OtherPermissions); + resultList[0].OtherPermissions, + resultList[0].OtherUserIsAdmin, + resultList[0].OtherUserIsModerator); } private async Task> GetAllPairInfo(string uid) @@ -408,7 +412,9 @@ public partial class LightlessHub GID = user.Gid, Synced = user.Synced, OwnPermissions = ownperm, - OtherPermissions = otherperm + OtherPermissions = otherperm, + OtherUserIsAdmin = u.IsAdmin, + OtherUserIsModerator = u.IsModerator }; var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); @@ -419,7 +425,10 @@ public partial class LightlessHub g.Max(p => p.Synced), g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), g.First().OwnPermissions, - g.First().OtherPermissions); + g.First().OtherPermissions, + g.First().OtherUserIsAdmin, + g.First().OtherUserIsModerator + ); }, StringComparer.Ordinal); } @@ -484,5 +493,14 @@ public partial class LightlessHub return await result.Distinct().AsNoTracking().ToListAsync().ConfigureAwait(false); } - public record UserInfo(string Alias, bool IndividuallyPaired, bool IsSynced, List GIDs, UserPermissionSet? OwnPermissions, UserPermissionSet? OtherPermissions); + public record UserInfo( + string Alias, + bool IndividuallyPaired, + bool IsSynced, + List GIDs, + UserPermissionSet? OwnPermissions, + UserPermissionSet? OtherPermissions, + bool IsAdmin, + bool IsModerator + ); } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index f9895ff..0a529eb 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -1,6 +1,8 @@ -using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; using LightlessSyncShared.Models; using LightlessSyncShared.Utils; @@ -319,6 +321,11 @@ public partial class LightlessHub return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); } + private static bool IsHex(char c) => + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + [Authorize(Policy = "Identified")] public async Task GroupJoinFinalize(GroupJoinDto dto) { @@ -329,7 +336,9 @@ public partial class LightlessHub var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); - var hashedPw = StringUtils.Sha256String(dto.Password); + var hashedPw = dto.Password.Length == 64 && dto.Password.All(IsHex) + ? dto.Password + : StringUtils.Sha256String(dto.Password); var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); @@ -523,6 +532,88 @@ public partial class LightlessHub return true; } + [Authorize(Policy = "Identified")] + public async Task GroupJoinHashed(GroupJoinHashedDto dto) + { + var aliasOrGid = dto.Group.GID.Trim(); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + var group = await DbContext.Groups.Include(g => g.Owner) + .AsNoTracking() + .SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid) + .ConfigureAwait(false); + + var groupGid = group?.GID ?? string.Empty; + + var existingPair = await DbContext.GroupPairs + .AsNoTracking() + .SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID) + .ConfigureAwait(false); + + var isBanned = await DbContext.GroupBans + .AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID) + .ConfigureAwait(false); + + var oneTimeInvite = await DbContext.GroupTempInvites + .SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == dto.HashedPassword) + .ConfigureAwait(false); + + var existingUserCount = await DbContext.GroupPairs + .AsNoTracking() + .CountAsync(g => g.GroupGID == groupGid) + .ConfigureAwait(false); + + var joinedGroups = await DbContext.GroupPairs + .CountAsync(g => g.GroupUserUID == UserUID) + .ConfigureAwait(false); + + if (group == null) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Syncshell not found."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (!string.Equals(group.HashedPassword, dto.HashedPassword, StringComparison.Ordinal) && oneTimeInvite == null) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Incorrect or expired password."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (existingPair != null) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "You are already a member of this syncshell."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (existingUserCount >= _maxGroupUserCount) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "This syncshell is full."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (!group.InvitesEnabled) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "Invites to this syncshell are currently disabled."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (joinedGroups >= _maxJoinedGroupsByUser) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "You have reached the maximum number of syncshells you can join."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + if (isBanned) + { + await Clients.User(UserUID).Client_ReceiveServerMessage(MessageSeverity.Warning, "You are banned from this syncshell."); + return new GroupJoinInfoDto(null, null, GroupPermissions.NoneSet, false); + } + + return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); + } + + [Authorize(Policy = "Identified")] public async Task GroupLeave(GroupDto dto) { @@ -669,4 +760,70 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); } + + [Authorize(Policy = "Identified")] + public async Task SetGroupBroadcastStatus(GroupBroadcastRequestDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + if (string.IsNullOrEmpty(dto.HashedCID)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("missing CID in syncshell broadcast request", "User", UserUID, "GID", dto.GID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Internal error: missing CID."); + return false; + } + + var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false); + if (!isOwner) + { + _logger.LogCallWarning(LightlessHubLogger.Args("Unauthorized syncshell broadcast change", "User", UserUID, "GID", dto.GID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You must be the owner of the syncshell to broadcast it."); + return false; + } + + return true; + } + + [Authorize(Policy = "Identified")] + public async Task> GetBroadcastedGroups(List broadcastEntries) + { + _logger.LogCallInfo(LightlessHubLogger.Args("Requested Syncshells", broadcastEntries.Select(b => b.GID))); + + var results = new List(); + var gidsToValidate = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in broadcastEntries) + { + if (string.IsNullOrWhiteSpace(entry.HashedCID) || string.IsNullOrWhiteSpace(entry.GID)) + continue; + + var redisKey = $"broadcast:{entry.HashedCID}"; + var redisEntry = await _redis.GetAsync(redisKey).ConfigureAwait(false); + + if (redisEntry?.GID != null && string.Equals(redisEntry.GID, entry.GID, StringComparison.OrdinalIgnoreCase)) + gidsToValidate.Add(entry.GID); + } + + if (gidsToValidate.Count == 0) + return results; + + var groups = await DbContext.Groups + .AsNoTracking() + .Where(g => gidsToValidate.Contains(g.GID) && g.InvitesEnabled) + .ToListAsync() + .ConfigureAwait(false); + + foreach (var group in groups) + { + results.Add(new GroupJoinDto( + Group: new GroupData(group.GID, group.Alias), + Password: group.HashedPassword, + GroupUserPreferredPermissions: new GroupUserPreferredPermissions() + )); + } + + return results; + } + + } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs index 53162a2..17dc027 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs @@ -1,9 +1,7 @@ -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; using LightlessSyncShared.Metrics; @@ -13,6 +11,10 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using StackExchange.Redis; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; namespace LightlessSyncServer.Hubs; @@ -137,6 +139,322 @@ public partial class LightlessHub } } + [Authorize(Policy = "Identified")] + public async Task TryPairWithContentId(string otherCid, string myCid) + { + if (string.IsNullOrWhiteSpace(otherCid) || string.IsNullOrWhiteSpace(myCid)) + return; + + if (string.Equals(otherCid, myCid, StringComparison.Ordinal)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can't pair with yourself.").ConfigureAwait(false); + return; + } + + var throttleKey = $"pairing:rate:{UserUID}"; + var existingThrottle = await _redis.GetAsync(throttleKey).ConfigureAwait(false); + if (existingThrottle != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You're sending requests too quickly. Please wait a moment.").ConfigureAwait(false); + return; + } + await _redis.AddAsync(throttleKey, "true", TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + var reverseKey = $"pairing:{otherCid}:{myCid}"; + var forwardKey = $"pairing:{myCid}:{otherCid}"; + + var json = await _redis.GetAsync(reverseKey).ConfigureAwait(false); + if (json != null) + { + await _redis.RemoveAsync(reverseKey).ConfigureAwait(false); + + try + { + var payload = JsonSerializer.Deserialize(json); + if (payload?.UID == null) + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid payload", reverseKey)); + return; + } + + var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID); + var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID); + + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + var otherUser = await DbContext.Users.SingleAsync(u => u.UID == payload.UID).ConfigureAwait(false); + + var pairData = await GetPairInfo(UserUID, payload.UID).ConfigureAwait(false); + var permissions = await DbContext.Permissions.SingleAsync(p => + p.UserUID == UserUID && p.OtherUserUID == payload.UID).ConfigureAwait(false); + + var ownPerm = permissions.ToUserPermissions(setSticky: true); + var otherPerm = pairData?.OtherPermissions.ToUserPermissions() ?? new UserPermissions(); + + var individualPairStatus = pairData?.IsSynced == true + ? IndividualPairStatus.Bidirectional + : IndividualPairStatus.OneSided; + + var dtoA = new UserPairDto(otherUser.ToUserData(), individualPairStatus, ownPerm, otherPerm); + var dtoB = new UserPairDto(user.ToUserData(), individualPairStatus, otherPerm, ownPerm); + + await Clients.User(UserUID).Client_UserAddClientPair(dtoA).ConfigureAwait(false); + await Clients.User(payload.UID).Client_UserAddClientPair(dtoB).ConfigureAwait(false); + + await Clients.User(payload.UID) + .Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(user.ToUserData(), permissions.ToUserPermissions())) + .ConfigureAwait(false); + + await Clients.User(payload.UID) + .Client_UpdateUserIndividualPairStatusDto(new(user.ToUserData(), individualPairStatus)) + .ConfigureAwait(false); + + await Clients.User(UserUID) + .Client_UpdateUserIndividualPairStatusDto(new(otherUser.ToUserData(), individualPairStatus)) + .ConfigureAwait(false); + + if (!ownPerm.IsPaused() && !otherPerm.IsPaused()) + { + var ident_sender = await GetUserIdent(UserUID).ConfigureAwait(false); + var ident_receiver = await GetUserIdent(payload.UID).ConfigureAwait(false); + + if (ident_sender != null && ident_receiver != null) + { + await Clients.User(UserUID).Client_UserSendOnline(new(otherUser.ToUserData(), ident_receiver)).ConfigureAwait(false); + await Clients.User(payload.UID).Client_UserSendOnline(new(user.ToUserData(), ident_sender)).ConfigureAwait(false); + } + } + + if (sender || receiver) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"paired with {payload.UID}.").ConfigureAwait(false); + await Clients.User(payload.UID).Client_ReceiveServerMessage(MessageSeverity.Information, $"paired with {UserUID}.").ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args("pair established", UserUID, payload.UID)); + } + + await _redis.RemoveAsync(forwardKey).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to process pairing", reverseKey, ex.Message)); + } + } + else + { + var payload = new PairingPayload + { + UID = UserUID, + Timestamp = DateTime.UtcNow + }; + + var payloadJson = JsonSerializer.Serialize(payload); + await _redis.AddAsync(forwardKey, payloadJson, TimeSpan.FromMinutes(5)).ConfigureAwait(false); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args("stored pairing request", myCid, otherCid)); + } + } + + private class PairingPayload + { + public string UID { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } + } + + public class BroadcastRedisEntry + { + public string HashedCID { get; set; } = string.Empty; + public string? GID { get; set; } + } + + [Authorize(Policy = "Identified")] + public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + { + var db = _redis.Database; + var broadcastKey = $"broadcast:{hashedCid}"; + + if (enabled) + { + string? gid = null; + + if (groupDto is not null) + { + groupDto.HashedCID = hashedCid; + + var valid = await SetGroupBroadcastStatus(groupDto).ConfigureAwait(false); + if (!valid) + return; + + gid = groupDto.GID; + } + + var entry = new BroadcastRedisEntry + { + HashedCID = hashedCid, + GID = gid, + }; + + var json = JsonSerializer.Serialize(entry); + await db.StringSetAsync(broadcastKey, json, TimeSpan.FromMinutes(5)).ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid)); + } + else + { + var value = await db.StringGetAsync(broadcastKey).ConfigureAwait(false); + if (value.IsNullOrEmpty) + return; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(value!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast entry during removal", "CID", hashedCid, "Value", value, "Error", ex)); + return; + } + + if (entry is null || entry.HashedCID != hashedCid) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Stored", entry?.HashedCID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + return; + } + + await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID)); + } + } + + + [Authorize(Policy = "Identified")] + public async Task IsUserBroadcasting(string hashedCid) + { + var db = _redis.Database; + var key = $"broadcast:{hashedCid}"; + + var result = await db.StringGetWithExpiryAsync(key).ConfigureAwait(false); + if (result.Expiry is null || result.Expiry <= TimeSpan.Zero || result.Value.IsNullOrEmpty) + return null; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(result.Value!); + } + catch + { + return null; + } + + var dto = new BroadcastStatusInfoDto + { + HashedCID = entry?.HashedCID ?? hashedCid, + IsBroadcasting = true, + TTL = result.Expiry, + GID = entry?.GID + }; + + _logger.LogCallInfo(LightlessHubLogger.Args("checked broadcast status", hashedCid, "TTL", result.Expiry, "GID", dto.GID)); + return dto; + } + + [Authorize(Policy = "Identified")] + public async Task GetBroadcastTtl(string hashedCid) + { + var db = _redis.Database; + var key = $"broadcast:{hashedCid}"; + + var value = await db.StringGetAsync(key).ConfigureAwait(false); + if (value.IsNullOrEmpty) + return null; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(value!); + } + catch + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid broadcast entry format", "CID", hashedCid)); + return null; + } + + if (entry?.HashedCID != hashedCid) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "EntryCID", entry?.HashedCID)); + return null; + } + + var ttl = await db.KeyTimeToLiveAsync(key).ConfigureAwait(false); + if (ttl is null || ttl <= TimeSpan.Zero) + return null; + + _logger.LogCallInfo(LightlessHubLogger.Args("checked broadcast ttl", UserUID, "CID", hashedCid, "TTL", ttl, "GID", entry.GID)); + return ttl; + } + + + private const int MaxBatchSize = 30; + + [Authorize(Policy = "Identified")] + public async Task AreUsersBroadcasting(List hashedCids) + { + var db = _redis.Database; + if (hashedCids.Count > MaxBatchSize) + hashedCids = hashedCids.Take(MaxBatchSize).ToList(); + + var tasks = new Dictionary>(hashedCids.Count); + foreach (var cid in hashedCids) + { + var key = $"broadcast:{cid}"; + tasks[cid] = db.StringGetWithExpiryAsync(key); + } + + await Task.WhenAll(tasks.Values).ConfigureAwait(false); + + var results = new Dictionary(StringComparer.Ordinal); + + foreach (var (cid, task) in tasks) + { + var result = task.Result; + var raw = result.Value; + TimeSpan? ttl = result.Expiry; + + BroadcastRedisEntry? entry = null; + string? gid = null; + bool isBroadcasting = false; + + if (!raw.IsNullOrEmpty && ttl > TimeSpan.Zero) + { + isBroadcasting = true; + + try + { + entry = JsonSerializer.Deserialize(raw!); + gid = entry?.GID; + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("deserialization failed", "CID", cid, "Raw", raw.ToString(), "Error", ex)); + } + } + + results[cid] = new BroadcastStatusInfoDto + { + HashedCID = entry?.HashedCID ?? cid, + IsBroadcasting = isBroadcasting, + TTL = ttl, + GID = gid, + }; + } + + _logger.LogCallInfo(LightlessHubLogger.Args("batch checked broadcast", "Count", hashedCids.Count)); + return new BroadcastStatusBatchDto { Results = results }; + } + + [Authorize(Policy = "Identified")] public async Task UserDelete() { @@ -175,7 +493,7 @@ public partial class LightlessHub var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false); return pairs.Select(p => { - return new UserFullPairDto(new UserData(p.Key, p.Value.Alias), + return new UserFullPairDto(new UserData(p.Key, p.Value.Alias, p.Value.IsAdmin, p.Value.IsModerator), p.Value.ToIndividualPairStatus(), p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(), p.Value.OwnPermissions.ToUserPermissions(setSticky: true), diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs index adf1fe0..17ca1e3 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs @@ -24,6 +24,7 @@ public partial class LightlessHub : Hub, ILightlessHub private static readonly ConcurrentDictionary _userConnections = new(StringComparer.Ordinal); private readonly LightlessMetrics _lightlessMetrics; private readonly SystemInfoService _systemInfoService; + private readonly PairService _pairService; private readonly IHttpContextAccessor _contextAccessor; private readonly LightlessHubLogger _logger; private readonly string _shardName; @@ -45,7 +46,7 @@ public partial class LightlessHub : Hub, ILightlessHub IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, IConfigurationService configuration, IHttpContextAccessor contextAccessor, IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus, - GPoseLobbyDistributionService gPoseLobbyDistributionService) + GPoseLobbyDistributionService gPoseLobbyDistributionService, PairService pairService) { _lightlessMetrics = lightlessMetrics; _systemInfoService = systemInfoService; @@ -64,6 +65,7 @@ public partial class LightlessHub : Hub, ILightlessHub _gPoseLobbyDistributionService = gPoseLobbyDistributionService; _logger = new LightlessHubLogger(this, logger); _dbContextLazy = new Lazy(() => lightlessDbContextFactory.CreateDbContext()); + _pairService = pairService; } protected override void Dispose(bool disposing) diff --git a/LightlessSyncServer/LightlessSyncServer/Services/PairService.cs b/LightlessSyncServer/LightlessSyncServer/Services/PairService.cs new file mode 100644 index 0000000..854472f --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/PairService.cs @@ -0,0 +1,108 @@ +using LightlessSyncShared.Data; +using LightlessSyncShared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +public class PairService +{ + private readonly IDbContextFactory _dbFactory; + private readonly ILogger _logger; + + public PairService(IDbContextFactory dbFactory, ILogger logger) + { + _dbFactory = dbFactory; + _logger = logger; + } + + public async Task TryAddPairAsync(string userUid, string otherUid) + { + if (userUid == otherUid || string.IsNullOrWhiteSpace(userUid) || string.IsNullOrWhiteSpace(otherUid)) + return false; + + await using var db = await _dbFactory.CreateDbContextAsync(); + + var user = await db.Users.SingleOrDefaultAsync(u => u.UID == userUid); + var other = await db.Users.SingleOrDefaultAsync(u => u.UID == otherUid); + + if (user == null || other == null) + return false; + + bool modified = false; + + if (!await db.ClientPairs.AnyAsync(p => p.UserUID == userUid && p.OtherUserUID == otherUid)) + { + db.ClientPairs.Add(new ClientPair + { + UserUID = userUid, + OtherUserUID = otherUid + }); + modified = true; + } + + if (!await db.ClientPairs.AnyAsync(p => p.UserUID == otherUid && p.OtherUserUID == userUid)) + { + db.ClientPairs.Add(new ClientPair + { + UserUID = otherUid, + OtherUserUID = userUid + }); + modified = true; + } + + if (!await db.Permissions.AnyAsync(p => p.UserUID == userUid && p.OtherUserUID == otherUid)) + { + var defaultPerms = await db.UserDefaultPreferredPermissions + .SingleOrDefaultAsync(p => p.UserUID == userUid); + + if (defaultPerms != null) + { + db.Permissions.Add(new UserPermissionSet + { + UserUID = userUid, + OtherUserUID = otherUid, + DisableAnimations = defaultPerms.DisableIndividualAnimations, + DisableSounds = defaultPerms.DisableIndividualSounds, + DisableVFX = defaultPerms.DisableIndividualVFX, + IsPaused = false, + Sticky = true, + }); + modified = true; + } + } + + if (!await db.Permissions.AnyAsync(p => p.UserUID == otherUid && p.OtherUserUID == userUid)) + { + var defaultPerms = await db.UserDefaultPreferredPermissions + .SingleOrDefaultAsync(p => p.UserUID == otherUid); + + if (defaultPerms != null) + { + db.Permissions.Add(new UserPermissionSet + { + UserUID = otherUid, + OtherUserUID = userUid, + DisableAnimations = defaultPerms.DisableIndividualAnimations, + DisableSounds = defaultPerms.DisableIndividualSounds, + DisableVFX = defaultPerms.DisableIndividualVFX, + IsPaused = false, + Sticky = true, + }); + modified = true; + } + } + + if (modified) + { + await db.SaveChangesAsync(); + _logger.LogInformation("Mutual pair established between {UserUID} and {OtherUID}", userUid, otherUid); + } + else + { + _logger.LogInformation("Pair already exists between {UserUID} and {OtherUID}", userUid, otherUid); + } + + return modified; + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Startup.cs b/LightlessSyncServer/LightlessSyncServer/Startup.cs index d9bab3f..5b0b672 100644 --- a/LightlessSyncServer/LightlessSyncServer/Startup.cs +++ b/LightlessSyncServer/LightlessSyncServer/Startup.cs @@ -105,6 +105,7 @@ public class Startup services.AddSingleton(); services.AddHostedService(provider => provider.GetService()); services.AddHostedService(); + services.AddScoped(); } services.AddSingleton(); From 71c01461aed6d1ace7e52f3efbe72f0190cd2800 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 24 Sep 2025 15:04:02 +0200 Subject: [PATCH 09/40] Lightfinder merge into group changes --- LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 24bb313..d174224 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -719,7 +719,7 @@ public partial class LightlessHub .ToDictionary(u => groups.First(f => string.Equals(f.GroupGID, u.GroupGID, StringComparison.Ordinal)), u => u); var groupInfos = await DbContext.GroupPairs.Where(u => groups.Select(g => g.GroupGID).Contains(u.GroupGID) && (u.IsPinned || u.IsModerator)) .ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - + return preferredPermissions.Select(g => new GroupFullInfoDto(g.Key.Group.ToGroupData(), g.Key.Group.Owner.ToUserData(), g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), g.Key.JoinedGroupOn, groupInfos.Where(i => string.Equals(i.GroupGID, g.Key.GroupGID, StringComparison.Ordinal)) From 03f633a2735ac3f2cd88d8726fa5767c8716d6b5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 02:03:15 +0200 Subject: [PATCH 10/40] Added lightless finder in dto of joining --- LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 1e157d1..2630a8f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -370,6 +370,7 @@ public partial class LightlessHub GroupGID = group.GID, GroupUserUID = UserUID, JoinedGroupOn = DateTime.UtcNow, + FromFinder = dto.Finder }; var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); @@ -617,7 +618,6 @@ public partial class LightlessHub return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); } - [Authorize(Policy = "Identified")] public async Task GroupLeave(GroupDto dto) { From 1a97dded9c8f770e607de6966f8d1a470f2bdde9 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 03:14:09 +0200 Subject: [PATCH 11/40] Removal of an accidental change I did. --- LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs index a0b7de8..f04db3f 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupPair.cs @@ -4,7 +4,6 @@ public class GroupPair { public string GroupGID { get; set; } public Group Group { get; set; } - public GroupProfile Profile { get; set; } public string GroupUserUID { get; set; } public User GroupUser { get; set; } public bool IsPinned { get; set; } From 3a4a934d09189a2619f6d7ecd6a8608aad74b60e Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 03:37:13 +0200 Subject: [PATCH 12/40] Revert of GroupFullInfoDto --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 2630a8f..c7a4741 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -211,9 +211,9 @@ public partial class LightlessHub var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), - newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), initialPair.JoinedGroupOn, new(StringComparer.Ordinal))) + newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal))) .ConfigureAwait(false); - + _logger.LogCallInfo(LightlessHubLogger.Args(gid)); return new GroupJoinDto(newGroup.ToGroupData(), passwd, initialPrefPermissions.ToEnum()); @@ -406,7 +406,7 @@ public partial class LightlessHub var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), - group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), newPair.JoinedGroupOn, + group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false); var self = DbContext.Users.Single(u => u.UID == UserUID); @@ -812,7 +812,7 @@ public partial class LightlessHub .ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); return preferredPermissions.Select(g => new GroupFullInfoDto(g.Key.Group.ToGroupData(), g.Key.Group.Owner.ToUserData(), - g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), g.Key.JoinedGroupOn, + g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), groupInfos.Where(i => string.Equals(i.GroupGID, g.Key.GroupGID, StringComparison.Ordinal)) .ToDictionary(i => i.GroupUserUID, i => i.ToEnum(), StringComparer.Ordinal))).ToList(); } From bf380688c83f0c750fe5903b8e223a5167868893 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 04:28:52 +0200 Subject: [PATCH 13/40] Redoing of groupsgetall --- .../Hubs/MareHub.Groups.cs | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index c7a4741..7c1fc0a 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -192,6 +192,7 @@ public partial class LightlessHub GroupUserUID = UserUID, IsPinned = true, JoinedGroupOn = currentTime, + FromFinder = false, }; GroupPairPreferredPermission initialPrefPermissions = new() @@ -804,17 +805,42 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var preferredPermissions = (await DbContext.GroupPairPreferredPermissions.Where(u => u.UserUID == UserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) - .Where(u => groups.Exists(k => string.Equals(k.GroupGID, u.GroupGID, StringComparison.Ordinal))) - .ToDictionary(u => groups.First(f => string.Equals(f.GroupGID, u.GroupGID, StringComparison.Ordinal)), u => u); - var groupInfos = await DbContext.GroupPairs.Where(u => groups.Select(g => g.GroupGID).Contains(u.GroupGID) && (u.IsPinned || u.IsModerator)) - .ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - - return preferredPermissions.Select(g => new GroupFullInfoDto(g.Key.Group.ToGroupData(), g.Key.Group.Owner.ToUserData(), - g.Key.Group.ToEnum(), g.Value.ToEnum(), g.Key.ToEnum(), - groupInfos.Where(i => string.Equals(i.GroupGID, g.Key.GroupGID, StringComparison.Ordinal)) - .ToDictionary(i => i.GroupUserUID, i => i.ToEnum(), StringComparer.Ordinal))).ToList(); + var ct = _contextAccessor.HttpContext.RequestAborted; + + var result = await ( + from gp in DbContext.GroupPairs + .Include(gp => gp.Group) + .ThenInclude(g => g.Owner) + join pp in DbContext.GroupPairPreferredPermissions + on new { gp.GroupGID, UserUID } equals new { pp.GroupGID, pp.UserUID } + where gp.GroupUserUID == UserUID + select new + { + GroupPair = gp, + PreferredPermission = pp, + GroupInfos = DbContext.GroupPairs + .Where(x => x.GroupGID == gp.GroupGID && (x.IsPinned || x.IsModerator)) + .Select(x => new { x.GroupUserUID, EnumValue = x.ToEnum() }) + .ToList() // forces subquery per row + }) + .AsNoTracking() + .ToListAsync(ct) + .ConfigureAwait(false); + + return [.. result.Select(r => + { + var groupInfoDict = r.GroupInfos + .ToDictionary(x => x.GroupUserUID, x => x.EnumValue, StringComparer.Ordinal); + + return new GroupFullInfoDto( + r.GroupPair.Group.ToGroupData(), + r.GroupPair.Group.Owner.ToUserData(), + r.GroupPair.Group.ToEnum(), + r.PreferredPermission.ToEnum(), + r.GroupPair.ToEnum(), + groupInfoDict + ); + })]; } [Authorize(Policy = "Identified")] From 825bb3b7d692cf2beb36f140e62294556d22796a Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 04:34:25 +0200 Subject: [PATCH 14/40] Cleaned up a bit. --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 7c1fc0a..5e02ba4 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -821,13 +821,13 @@ public partial class LightlessHub GroupInfos = DbContext.GroupPairs .Where(x => x.GroupGID == gp.GroupGID && (x.IsPinned || x.IsModerator)) .Select(x => new { x.GroupUserUID, EnumValue = x.ToEnum() }) - .ToList() // forces subquery per row + .ToList(), }) .AsNoTracking() .ToListAsync(ct) .ConfigureAwait(false); - return [.. result.Select(r => + List List = [.. result.Select(r => { var groupInfoDict = r.GroupInfos .ToDictionary(x => x.GroupUserUID, x => x.EnumValue, StringComparer.Ordinal); @@ -840,7 +840,8 @@ public partial class LightlessHub r.GroupPair.ToEnum(), groupInfoDict ); - })]; + }),]; + return List; } [Authorize(Policy = "Identified")] From 81e773e0c46b9c2b1d87124fd034f261e8cc94e6 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 05:24:07 +0200 Subject: [PATCH 15/40] Updated submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 96744f4..a337481 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 96744f4f28142a5cd40103d6ee209761b354b818 +Subproject commit a337481243a11490f3a115ca1ac0abfdd62c0554 From 7fecea2c6fa69dc9216cbf2501a89c86cbba5738 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 05:24:54 +0200 Subject: [PATCH 16/40] Updated Submodule to groups --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index a337481..69055b0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit a337481243a11490f3a115ca1ac0abfdd62c0554 +Subproject commit 69055b0f323e6d35f55750fd1dc5659a8e36b085 From f9f25829a033edc64104c213e6ac2d239c594e2d Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 25 Sep 2025 10:00:16 -0500 Subject: [PATCH 17/40] Add logging to test --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 5e02ba4..f4e364f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -827,11 +827,16 @@ public partial class LightlessHub .ToListAsync(ct) .ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args(result)); + List List = [.. result.Select(r => { var groupInfoDict = r.GroupInfos .ToDictionary(x => x.GroupUserUID, x => x.EnumValue, StringComparer.Ordinal); + + _logger.LogCallInfo(LightlessHubLogger.Args(r)); + return new GroupFullInfoDto( r.GroupPair.Group.ToGroupData(), r.GroupPair.Group.Owner.ToUserData(), From 39aded4fb7dab5820b78aad1db24f69d2257ef58 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 17:42:09 +0200 Subject: [PATCH 18/40] Changed prefix of syncshells from MSS to LSS --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index f4e364f..768fe21 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -161,11 +161,11 @@ public partial class LightlessHub } var gid = StringUtils.GenerateRandomString(12); - while (await DbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) + while (await DbContext.Groups.AnyAsync(g => g.GID == "LSS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) { gid = StringUtils.GenerateRandomString(12); } - gid = "MSS-" + gid; + gid = "LSS-" + gid; var passwd = StringUtils.GenerateRandomString(16); using var sha = SHA256.Create(); From f084837e012af5c6eebb76ecdec8020d058b8dc5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 18:04:50 +0200 Subject: [PATCH 19/40] Updated submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 69055b0..d62adbb 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 69055b0f323e6d35f55750fd1dc5659a8e36b085 +Subproject commit d62adbb5b61ee12fc62e43cd70fb679eb2bcd1e4 From e470e5346af183a957077c951bef2315feada770 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 18:14:49 +0200 Subject: [PATCH 20/40] Typo --- .../LightlessSyncServer/Hubs/MareHub.Groups.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index 768fe21..e66d4b4 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -161,11 +161,11 @@ public partial class LightlessHub } var gid = StringUtils.GenerateRandomString(12); - while (await DbContext.Groups.AnyAsync(g => g.GID == "LSS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) + while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) { gid = StringUtils.GenerateRandomString(12); } - gid = "LSS-" + gid; + gid = "LLS-" + gid; var passwd = StringUtils.GenerateRandomString(16); using var sha = SHA256.Create(); From 4004cf289e65a84ba026e10fb4acc73bd5d36d7c Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 04:31:42 +0900 Subject: [PATCH 21/40] clear lightfinder joiners --- .../Hubs/MareHub.Groups.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs index e66d4b4..9713831 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs @@ -149,6 +149,52 @@ public partial class LightlessHub await DbContext.SaveChangesAsync().ConfigureAwait(false); } + [Authorize(Policy = "Identified")] + public async Task GroupClearFinder(GroupDto dto) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + + var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); + if (!hasRights) return; + + var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false); + var finder_only = groupPairs.Where(g => g.FromFinder && !g.IsPinned && !g.IsModerator).ToList(); + + if (finder_only.Count == 0) + { + _logger.LogCallInfo(LightlessHubLogger.Args(dto, "No Users To Clear")); + return; + } + + await Clients.Users(finder_only.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); + + _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Cleared Finder users ", finder_only.Count)); + + DbContext.GroupPairs.RemoveRange(finder_only); + + foreach (var pair in finder_only) + { + await Clients.Users(groupPairs.Where(p => p.IsPinned || p.IsModerator).Select(g => g.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); + + var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false); + if (string.IsNullOrEmpty(pairIdent)) continue; + + var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); + + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + + DbContext.CharaDataAllowances.RemoveRange(sharedData); + + foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) + { + await UserGroupLeave(pair, pairIdent, allUserPairs, pair.GroupUserUID).ConfigureAwait(false); + } + } + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + } + + [Authorize(Policy = "Identified")] public async Task GroupCreate() { @@ -340,7 +386,8 @@ public partial class LightlessHub var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); - var hashedPw = dto.Password.Length == 64 && dto.Password.All(IsHex) + var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(IsHex); + var hashedPw = isHashedPassword ? dto.Password : StringUtils.Sha256String(dto.Password); var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); @@ -371,7 +418,7 @@ public partial class LightlessHub GroupGID = group.GID, GroupUserUID = UserUID, JoinedGroupOn = DateTime.UtcNow, - FromFinder = dto.Finder + FromFinder = isHashedPassword }; var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); From c4b6e85f60e936de63f478b90df3c51b8d8459d3 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 04:37:41 +0900 Subject: [PATCH 22/40] bump submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index d62adbb..3c10380 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit d62adbb5b61ee12fc62e43cd70fb679eb2bcd1e4 +Subproject commit 3c10380162b162c47c99f63ecfc627a49887fe84 From 323d3f39e259c52d68a791cf25166d3eaf50ade2 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 08:26:24 +0900 Subject: [PATCH 23/40] uid colors --- ...0925225205_AddUserVanityFields.Designer.cs | 1172 +++++++++++++++++ .../20250925225205_AddUserVanityFields.cs | 53 + .../LightlessDbContextModelSnapshot.cs | 19 +- .../LightlessSyncShared/Models/User.cs | 10 +- 4 files changed, 1252 insertions(+), 2 deletions(-) create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs new file mode 100644 index 0000000..201cc8b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs @@ -0,0 +1,1172 @@ +// +using System; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20250925225205_AddUserVanityFields")] + partial class AddUserVanityFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("MarkForBan") + .HasColumnType("boolean") + .HasColumnName("mark_for_ban"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("BannedUid") + .HasColumnType("text") + .HasColumnName("banned_uid"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedGroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedGroupGID") + .HasDatabaseName("ix_chara_data_allowance_allowed_group_gid"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("RawSize") + .HasColumnType("bigint") + .HasColumnName("raw_size"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.Property("PreferDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_animations"); + + b.Property("PreferDisableSounds") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_sounds"); + + b.Property("PreferDisableVFX") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_vfx"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("FromFinder") + .HasColumnType("boolean") + .HasColumnName("from_finder"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.Property("JoinedGroupOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_group_on"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("UserUID", "GroupGID") + .HasName("pk_group_pair_preferred_permissions"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pair_preferred_permissions_group_gid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_group_pair_preferred_permissions_user_uid"); + + b.ToTable("group_pair_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.Property("GroupGID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Base64GroupProfileImage") + .HasColumnType("text") + .HasColumnName("base64group_profile_image"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.HasKey("GroupGID") + .HasName("pk_group_profiles"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_profiles_group_gid"); + + b.ToTable("group_profiles", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("HasVanity") + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("has_vanity"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("TextColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasDefaultValue(string.Empty) + .HasColumnName("text_color_hex"); + + b.Property("TextGlowColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasDefaultValue(string.Empty) + .HasColumnName("text_glow_color_hex"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("DisableGroupAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_group_animations"); + + b.Property("DisableGroupSounds") + .HasColumnType("boolean") + .HasColumnName("disable_group_sounds"); + + b.Property("DisableGroupVFX") + .HasColumnType("boolean") + .HasColumnName("disable_group_vfx"); + + b.Property("DisableIndividualAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_individual_animations"); + + b.Property("DisableIndividualSounds") + .HasColumnType("boolean") + .HasColumnName("disable_individual_sounds"); + + b.Property("DisableIndividualVFX") + .HasColumnType("boolean") + .HasColumnName("disable_individual_vfx"); + + b.Property("IndividualIsSticky") + .HasColumnType("boolean") + .HasColumnName("individual_is_sticky"); + + b.HasKey("UserUID") + .HasName("pk_user_default_preferred_permissions"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_default_preferred_permissions_user_uid"); + + b.ToTable("user_default_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Sticky") + .HasColumnType("boolean") + .HasColumnName("sticky"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_user_permission_sets"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_user_permission_sets_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_permission_sets_user_uid"); + + b.HasIndex("UserUID", "OtherUserUID", "IsPaused") + .HasDatabaseName("ix_user_permission_sets_user_uid_other_user_uid_is_paused"); + + b.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedGroup"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_uid"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithOne("Profile") + .HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID") + .HasConstraintName("fk_group_profiles_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Navigation("Profile"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs new file mode 100644 index 0000000..0859d55 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AddUserVanityFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "has_vanity", + table: "users", + type: "boolean", + nullable: true, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "text_color_hex", + table: "users", + type: "character varying(9)", + maxLength: 9, + nullable: true, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "text_glow_color_hex", + table: "users", + type: "character varying(9)", + maxLength: 9, + nullable: true, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "has_vanity", + table: "users"); + + migrationBuilder.DropColumn( + name: "text_color_hex", + table: "users"); + + migrationBuilder.DropColumn( + name: "text_glow_color_hex", + table: "users"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index 01f6009..728a7c8 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using LightlessSyncShared.Data; using Microsoft.EntityFrameworkCore; @@ -683,6 +683,11 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(15)") .HasColumnName("alias"); + b.Property("HasVanity") + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("has_vanity"); + b.Property("IsAdmin") .HasColumnType("boolean") .HasColumnName("is_admin"); @@ -695,6 +700,18 @@ namespace LightlessSyncServer.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("last_logged_in"); + b.Property("TextColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasDefaultValue(string.Empty) + .HasColumnName("text_color_hex"); + + b.Property("TextGlowColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasDefaultValue(string.Empty) + .HasColumnName("text_glow_color_hex"); + b.Property("Timestamp") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() diff --git a/LightlessSyncServer/LightlessSyncShared/Models/User.cs b/LightlessSyncServer/LightlessSyncShared/Models/User.cs index fcaef37..f36fc84 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/User.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/User.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace LightlessSyncShared.Models; @@ -14,6 +14,14 @@ public class User public bool IsAdmin { get; set; } = false; + public bool? HasVanity { get; set; } = false; + + [MaxLength(9)] + public string? TextColorHex { get; set; } = string.Empty; + + [MaxLength(9)] + public string? TextGlowColorHex { get; set; } = string.Empty; + public DateTime LastLoggedIn { get; set; } [MaxLength(15)] public string Alias { get; set; } From 7271e007cda51e86d181008a547c185701cdc103 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Fri, 26 Sep 2025 03:48:24 +0200 Subject: [PATCH 24/40] Renamed hub --- .../Hubs/{MareHub.CharaData.cs => LightlessHub.CharaData.cs} | 0 .../Hubs/{MareHub.ClientStubs.cs => LightlessHub.ClientStubs.cs} | 0 .../Hubs/{MareHub.Functions.cs => LightlessHub.Functions.cs} | 0 .../Hubs/{MareHub.GposeLobby.cs => LightlessHub.GposeLobby.cs} | 0 .../Hubs/{MareHub.Groups.cs => LightlessHub.Groups.cs} | 0 .../Hubs/{MareHub.Permissions.cs => LightlessHub.Permissions.cs} | 0 .../Hubs/{MareHub.User.cs => LightlessHub.User.cs} | 0 .../LightlessSyncServer/Hubs/{MareHub.cs => LightlessHub.cs} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.CharaData.cs => LightlessHub.CharaData.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.ClientStubs.cs => LightlessHub.ClientStubs.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.Functions.cs => LightlessHub.Functions.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.GposeLobby.cs => LightlessHub.GposeLobby.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.Groups.cs => LightlessHub.Groups.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.Permissions.cs => LightlessHub.Permissions.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.User.cs => LightlessHub.User.cs} (100%) rename LightlessSyncServer/LightlessSyncServer/Hubs/{MareHub.cs => LightlessHub.cs} (100%) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.CharaData.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.CharaData.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.CharaData.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.CharaData.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.ClientStubs.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.ClientStubs.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.ClientStubs.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Functions.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.GposeLobby.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.GposeLobby.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.GposeLobby.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.GposeLobby.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Groups.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Permissions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Permissions.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.Permissions.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Permissions.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.User.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Hubs/MareHub.cs rename to LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs From f5d621e354ee22538960b0380e968a8cbd54365c Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:00:46 +0900 Subject: [PATCH 25/40] expose vanity and colors and update from bot --- LightlessAPI | 2 +- .../Hubs/LightlessHub.Functions.cs | 34 ++++++--- .../Hubs/LightlessHub.User.cs | 4 +- .../Discord/DiscordBot.cs | 76 ++++++++++++++++++- .../Discord/MareWizardModule.Vanity.cs | 35 ++++++++- 5 files changed, 135 insertions(+), 16 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 3c10380..eb04433 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 3c10380162b162c47c99f63ecfc627a49887fe84 +Subproject commit eb04433427d8b5144688004e436d833c6b63d39c diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index 6344f40..b9d5db8 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -1,4 +1,4 @@ -using LightlessSyncShared.Models; +using LightlessSyncShared.Models; using Microsoft.EntityFrameworkCore; using LightlessSyncServer.Utils; using LightlessSyncShared.Utils; @@ -325,7 +325,10 @@ public partial class LightlessHub OwnPermissions = ownperm, OtherPermissions = otherperm, OtherUserIsAdmin = u.IsAdmin, - OtherUserIsModerator = u.IsModerator + OtherUserIsModerator = u.IsModerator, + OtherUserHasVanity = u.HasVanity, + OtherUserTextColorHex = u.TextColorHex, + OtherUserTextGlowColorHex = u.TextGlowColorHex }; var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); @@ -333,14 +336,18 @@ public partial class LightlessHub if (!resultList.Any()) return null; var groups = resultList.Select(g => g.GID).ToList(); - return new UserInfo(resultList[0].OtherUserAlias, + return new UserInfo( + resultList[0].OtherUserAlias, resultList.SingleOrDefault(p => string.IsNullOrEmpty(p.GID))?.Synced ?? false, resultList.Max(p => p.Synced), resultList.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), resultList[0].OwnPermissions, resultList[0].OtherPermissions, resultList[0].OtherUserIsAdmin, - resultList[0].OtherUserIsModerator); + resultList[0].OtherUserIsModerator, + resultList[0].OtherUserHasVanity ?? false, + resultList[0].OtherUserTextColorHex ?? string.Empty, + resultList[0].OtherUserTextGlowColorHex ?? string.Empty); } private async Task> GetAllPairInfo(string uid) @@ -414,21 +421,27 @@ public partial class LightlessHub OwnPermissions = ownperm, OtherPermissions = otherperm, OtherUserIsAdmin = u.IsAdmin, - OtherUserIsModerator = u.IsModerator + OtherUserIsModerator = u.IsModerator, + OtherUserHasVanity = u.HasVanity, + OtherUserTextColorHex = u.TextColorHex, + OtherUserTextGlowColorHex = u.TextGlowColorHex }; var resultList = await result.AsNoTracking().ToListAsync().ConfigureAwait(false); return resultList.GroupBy(g => g.OtherUserUID, StringComparer.Ordinal).ToDictionary(g => g.Key, g => { - return new UserInfo(g.First().OtherUserAlias, + return new UserInfo( + g.First().OtherUserAlias, g.SingleOrDefault(p => string.IsNullOrEmpty(p.GID))?.Synced ?? false, g.Max(p => p.Synced), g.Select(p => string.IsNullOrEmpty(p.GID) ? Constants.IndividualKeyword : p.GID).ToList(), g.First().OwnPermissions, g.First().OtherPermissions, g.First().OtherUserIsAdmin, - g.First().OtherUserIsModerator - ); + g.First().OtherUserIsModerator, + g.First().OtherUserHasVanity ?? false, + g.First().OtherUserTextColorHex ?? string.Empty, + g.First().OtherUserTextGlowColorHex ?? string.Empty); }, StringComparer.Ordinal); } @@ -501,6 +514,9 @@ public partial class LightlessHub UserPermissionSet? OwnPermissions, UserPermissionSet? OtherPermissions, bool IsAdmin, - bool IsModerator + bool IsModerator, + bool HasVanity, + string? TextColorHex, + string? TextGlowColorHex ); } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index 27c72eb..b9fc652 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -1,4 +1,4 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; @@ -493,7 +493,7 @@ public partial class LightlessHub var pairs = await GetAllPairInfo(UserUID).ConfigureAwait(false); return pairs.Select(p => { - return new UserFullPairDto(new UserData(p.Key, p.Value.Alias, p.Value.IsAdmin, p.Value.IsModerator), + return new UserFullPairDto(new UserData(p.Key, p.Value.Alias, p.Value.IsAdmin, p.Value.IsModerator, p.Value.HasVanity, p.Value.TextColorHex, p.Value.TextGlowColorHex), p.Value.ToIndividualPairStatus(), p.Value.GIDs.Where(g => !string.Equals(g, Constants.IndividualKeyword, StringComparison.OrdinalIgnoreCase)).ToList(), p.Value.OwnPermissions.ToUserPermissions(setSticky: true), diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs index bbf96c4..ca2e015 100644 --- a/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs +++ b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.Interactions; using Discord.Rest; using Discord.WebSocket; @@ -384,13 +384,50 @@ internal class DiscordBot : IHostedService _logger.LogInformation($"Checking Group: {group.GID} [{group.Alias}], owned by {group.OwnerUID} ({groupPrimaryUser}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); - if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(allowedRoleIds.Keys.Contains)) + var hasAllowedRole = lodestoneUser != null && discordUser != null && discordUser.RoleIds.Any(allowedRoleIds.Keys.Contains); + + if (!hasAllowedRole) { await _botServices.LogToChannel($"VANITY GID REMOVAL: <@{lodestoneUser?.DiscordId ?? 0}> ({lodestoneUser?.User?.UID}) - GID: {group.GID}, Vanity: {group.Alias}").ConfigureAwait(false); _logger.LogInformation($"User {lodestoneUser?.User?.UID ?? "unknown"} not in allowed roles, deleting group alias for {group.GID}"); group.Alias = null; db.Update(group); + + if (lodestoneUser?.User != null) + { + lodestoneUser.User.HasVanity = false; + db.Update(lodestoneUser.User); + + var secondaryUsers = await db.Auth.Include(u => u.User) + .Where(u => u.PrimaryUserUID == lodestoneUser.User.UID).ToListAsync().ConfigureAwait(false); + + foreach (var secondaryUser in secondaryUsers) + { + secondaryUser.User.HasVanity = false; + db.Update(secondaryUser.User); + } + } + + await db.SaveChangesAsync(token).ConfigureAwait(false); + } + else if (lodestoneUser?.User != null && lodestoneUser.User.HasVanity != true) + { + lodestoneUser.User.HasVanity = true; + db.Update(lodestoneUser.User); + + var secondaryUsers = await db.Auth.Include(u => u.User) + .Where(u => u.PrimaryUserUID == lodestoneUser.User.UID).ToListAsync().ConfigureAwait(false); + + foreach (var secondaryUser in secondaryUsers) + { + if (secondaryUser.User.HasVanity != true) + { + secondaryUser.User.HasVanity = true; + db.Update(secondaryUser.User); + } + } + await db.SaveChangesAsync(token).ConfigureAwait(false); } } @@ -400,22 +437,55 @@ internal class DiscordBot : IHostedService var discordUser = await restGuild.GetUserAsync(lodestoneAuth.DiscordId).ConfigureAwait(false); _logger.LogInformation($"Checking User: {lodestoneAuth.DiscordId}, {lodestoneAuth.User.UID} ({lodestoneAuth.User.Alias}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); - if (discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Keys.Contains(u))) + var hasAllowedRole = discordUser != null && discordUser.RoleIds.Any(u => allowedRoleIds.Keys.Contains(u)); + + if (!hasAllowedRole) { _logger.LogInformation($"User {lodestoneAuth.User.UID} not in allowed roles, deleting alias"); await _botServices.LogToChannel($"VANITY UID REMOVAL: <@{lodestoneAuth.DiscordId}> - UID: {lodestoneAuth.User.UID}, Vanity: {lodestoneAuth.User.Alias}").ConfigureAwait(false); lodestoneAuth.User.Alias = null; + lodestoneAuth.User.HasVanity = false; var secondaryUsers = await db.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ToListAsync().ConfigureAwait(false); foreach (var secondaryUser in secondaryUsers) { _logger.LogInformation($"Secondary User {secondaryUser.User.UID} not in allowed roles, deleting alias"); secondaryUser.User.Alias = null; + secondaryUser.User.HasVanity = false; db.Update(secondaryUser.User); } db.Update(lodestoneAuth.User); await db.SaveChangesAsync(token).ConfigureAwait(false); } + else + { + var secondaryUsers = await db.Auth.Include(u => u.User) + .Where(u => u.PrimaryUserUID == lodestoneAuth.User.UID).ToListAsync().ConfigureAwait(false); + + var hasChanges = false; + + if (lodestoneAuth.User.HasVanity != true) + { + lodestoneAuth.User.HasVanity = true; + db.Update(lodestoneAuth.User); + hasChanges = true; + } + + foreach (var secondaryUser in secondaryUsers) + { + if (secondaryUser.User.HasVanity != true) + { + secondaryUser.User.HasVanity = true; + db.Update(secondaryUser.User); + hasChanges = true; + } + } + + if (hasChanges) + { + await db.SaveChangesAsync(token).ConfigureAwait(false); + } + } } private async Task UpdateStatusAsync(CancellationToken token) diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs index 917dafc..783bd56 100644 --- a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs @@ -1,4 +1,4 @@ -using Discord.Interactions; +using Discord.Interactions; using Discord; using Microsoft.EntityFrameworkCore; using System.Text.RegularExpressions; @@ -123,6 +123,20 @@ public partial class LightlessWizardModule { var user = await db.Users.SingleAsync(u => u.UID == uid).ConfigureAwait(false); user.Alias = desiredVanityUid; + user.HasVanity = true; + + var secondaryUsers = await db.Auth.Include(u => u.User) + .Where(u => u.PrimaryUserUID == user.UID).ToListAsync().ConfigureAwait(false); + + foreach (var secondaryUser in secondaryUsers) + { + if (secondaryUser.User.HasVanity != true) + { + secondaryUser.User.HasVanity = true; + db.Update(secondaryUser.User); + } + } + db.Update(user); await db.SaveChangesAsync().ConfigureAwait(false); eb.WithColor(Color.Green); @@ -199,6 +213,25 @@ public partial class LightlessWizardModule { var group = await db.Groups.SingleAsync(u => u.GID == gid).ConfigureAwait(false); group.Alias = desiredVanityGid; + + var ownerAuth = await db.Auth.SingleOrDefaultAsync(u => u.UserUID == group.OwnerUID).ConfigureAwait(false); + var ownerUid = string.IsNullOrEmpty(ownerAuth?.PrimaryUserUID) ? group.OwnerUID : ownerAuth.PrimaryUserUID; + var ownerUser = await db.Users.SingleAsync(u => u.UID == ownerUid).ConfigureAwait(false); + ownerUser.HasVanity = true; + db.Update(ownerUser); + + var secondaryUsers = await db.Auth.Include(u => u.User) + .Where(u => u.PrimaryUserUID == ownerUser.UID).ToListAsync().ConfigureAwait(false); + + foreach (var secondaryUser in secondaryUsers) + { + if (secondaryUser.User.HasVanity != true) + { + secondaryUser.User.HasVanity = true; + db.Update(secondaryUser.User); + } + } + db.Update(group); await db.SaveChangesAsync().ConfigureAwait(false); eb.WithColor(Color.Green); From 2b05223a4b4dc8926b6cca80c092c792109cdf65 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:53:47 +0900 Subject: [PATCH 26/40] added methods to update vanity colors and submodule bump --- LightlessAPI | 2 +- .../Hubs/LightlessHub.Functions.cs | 23 +++++ .../Hubs/LightlessHub.User.cs | 93 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index eb04433..b85b54f 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit eb04433427d8b5144688004e436d833c6b63d39c +Subproject commit b85b54f560d3d4d901b3af1421337a4b22b0d067 diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index b9d5db8..83a2c15 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -6,6 +6,7 @@ using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; using LightlessSyncShared.Metrics; using Microsoft.AspNetCore.SignalR; +using System.Threading; namespace LightlessSyncServer.Hubs; @@ -97,6 +98,28 @@ public partial class LightlessHub await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } + private async Task EnsureUserHasVanity(string uid, CancellationToken cancellationToken = default) + { + cancellationToken = cancellationToken == default && _contextAccessor.HttpContext != null + ? _contextAccessor.HttpContext.RequestAborted + : cancellationToken; + + var user = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid, cancellationToken).ConfigureAwait(false); + if (user == null) + { + _logger.LogCallWarning(LightlessHubLogger.Args("vanity check", uid, "missing user")); + return null; + } + + if (user.HasVanity != true) + { + _logger.LogCallWarning(LightlessHubLogger.Args("vanity check", uid, "no vanity")); + return null; + } + + return user; + } + private async Task SendGroupDeletedToAll(List groupUsers) { foreach (var pair in groupUsers) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index b9fc652..ddd0b1b 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -605,6 +605,62 @@ public partial class LightlessHub _lightlessMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipientUids.Count); } + [Authorize(Policy = "Identified")] + public async Task UserUpdateVanityColors(UserVanityColorsDto dto) + { + if (dto == null) + { + throw new HubException("Vanity color payload required"); + } + + _logger.LogCallInfo(LightlessHubLogger.Args(dto.TextColorHex, dto.TextGlowColorHex)); + + var cooldownKey = $"vanity:colors:{UserUID}"; + var existingCooldown = await _redis.GetAsync(cooldownKey).ConfigureAwait(false); + if (existingCooldown != null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can update vanity colors once per minute.").ConfigureAwait(false); + return; + } + + var user = await EnsureUserHasVanity(UserUID).ConfigureAwait(false); + if (user == null) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Vanity privileges are required to update colors.").ConfigureAwait(false); + return; + } + + if (!TryNormalizeColor(dto.TextColorHex, out var textColor, out var textColorError)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, textColorError).ConfigureAwait(false); + return; + } + + if (!TryNormalizeColor(dto.TextGlowColorHex, out var textGlowColor, out var textGlowError)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, textGlowError).ConfigureAwait(false); + return; + } + + var currentColor = user.TextColorHex ?? string.Empty; + var currentGlow = user.TextGlowColorHex ?? string.Empty; + + if (string.Equals(currentColor, textColor, StringComparison.Ordinal) && + string.Equals(currentGlow, textGlowColor, StringComparison.Ordinal)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Vanity colors are already set to these values.").ConfigureAwait(false); + return; + } + + user.TextColorHex = textColor; + user.TextGlowColorHex = textGlowColor; + + await DbContext.SaveChangesAsync().ConfigureAwait(false); + await _redis.AddAsync(cooldownKey, "true", TimeSpan.FromMinutes(1)).ConfigureAwait(false); + + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Vanity colors updated.").ConfigureAwait(false); + } + [Authorize(Policy = "Identified")] public async Task UserRemovePair(UserDto dto) { @@ -741,6 +797,43 @@ public partial class LightlessHub await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); } + private static bool TryNormalizeColor(string? value, out string normalized, out string errorMessage) + { + if (string.IsNullOrWhiteSpace(value)) + { + normalized = string.Empty; + errorMessage = string.Empty; + return true; + } + + var trimmed = value.Trim(); + if (trimmed.StartsWith("#", StringComparison.Ordinal)) + { + trimmed = trimmed[1..]; + } + + if (trimmed.Length != 6 && trimmed.Length != 8) + { + normalized = string.Empty; + errorMessage = "Colors must contain 6 or 8 hexadecimal characters."; + return false; + } + + foreach (var ch in trimmed) + { + if (!Uri.IsHexDigit(ch)) + { + normalized = string.Empty; + errorMessage = "Colors may only contain hexadecimal characters."; + return false; + } + } + + normalized = "#" + trimmed.ToUpperInvariant(); + errorMessage = string.Empty; + return true; + } + [GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] private static partial Regex GamePathRegex(); From 48cf492fa14914b63c2ee6356717798ea6258170 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:25:08 +0900 Subject: [PATCH 27/40] remove nullability --- .../LightlessSyncServer/Hubs/LightlessHub.Functions.cs | 6 +++--- .../LightlessSyncServices/Discord/DiscordBot.cs | 8 ++++---- .../Discord/MareWizardModule.Vanity.cs | 4 ++-- .../20250925225205_AddUserVanityFields.Designer.cs | 2 +- .../Migrations/20250925225205_AddUserVanityFields.cs | 2 +- .../Migrations/LightlessDbContextModelSnapshot.cs | 2 +- LightlessSyncServer/LightlessSyncShared/Models/User.cs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index 83a2c15..40ca003 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -111,7 +111,7 @@ public partial class LightlessHub return null; } - if (user.HasVanity != true) + if (!user.HasVanity) { _logger.LogCallWarning(LightlessHubLogger.Args("vanity check", uid, "no vanity")); return null; @@ -368,7 +368,7 @@ public partial class LightlessHub resultList[0].OtherPermissions, resultList[0].OtherUserIsAdmin, resultList[0].OtherUserIsModerator, - resultList[0].OtherUserHasVanity ?? false, + resultList[0].OtherUserHasVanity, resultList[0].OtherUserTextColorHex ?? string.Empty, resultList[0].OtherUserTextGlowColorHex ?? string.Empty); } @@ -462,7 +462,7 @@ public partial class LightlessHub g.First().OtherPermissions, g.First().OtherUserIsAdmin, g.First().OtherUserIsModerator, - g.First().OtherUserHasVanity ?? false, + g.First().OtherUserHasVanity, g.First().OtherUserTextColorHex ?? string.Empty, g.First().OtherUserTextGlowColorHex ?? string.Empty); }, StringComparer.Ordinal); diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs index ca2e015..4a2951f 100644 --- a/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs +++ b/LightlessSyncServer/LightlessSyncServices/Discord/DiscordBot.cs @@ -411,7 +411,7 @@ internal class DiscordBot : IHostedService await db.SaveChangesAsync(token).ConfigureAwait(false); } - else if (lodestoneUser?.User != null && lodestoneUser.User.HasVanity != true) + else if (lodestoneUser?.User != null && !lodestoneUser.User.HasVanity) { lodestoneUser.User.HasVanity = true; db.Update(lodestoneUser.User); @@ -421,7 +421,7 @@ internal class DiscordBot : IHostedService foreach (var secondaryUser in secondaryUsers) { - if (secondaryUser.User.HasVanity != true) + if (!secondaryUser.User.HasVanity) { secondaryUser.User.HasVanity = true; db.Update(secondaryUser.User); @@ -464,7 +464,7 @@ internal class DiscordBot : IHostedService var hasChanges = false; - if (lodestoneAuth.User.HasVanity != true) + if (!lodestoneAuth.User.HasVanity) { lodestoneAuth.User.HasVanity = true; db.Update(lodestoneAuth.User); @@ -473,7 +473,7 @@ internal class DiscordBot : IHostedService foreach (var secondaryUser in secondaryUsers) { - if (secondaryUser.User.HasVanity != true) + if (!secondaryUser.User.HasVanity) { secondaryUser.User.HasVanity = true; db.Update(secondaryUser.User); diff --git a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs index 783bd56..fb075e3 100644 --- a/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs +++ b/LightlessSyncServer/LightlessSyncServices/Discord/MareWizardModule.Vanity.cs @@ -130,7 +130,7 @@ public partial class LightlessWizardModule foreach (var secondaryUser in secondaryUsers) { - if (secondaryUser.User.HasVanity != true) + if (!secondaryUser.User.HasVanity) { secondaryUser.User.HasVanity = true; db.Update(secondaryUser.User); @@ -225,7 +225,7 @@ public partial class LightlessWizardModule foreach (var secondaryUser in secondaryUsers) { - if (secondaryUser.User.HasVanity != true) + if (!secondaryUser.User.HasVanity) { secondaryUser.User.HasVanity = true; db.Update(secondaryUser.User); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs index 201cc8b..c667a50 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs @@ -686,7 +686,7 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(15)") .HasColumnName("alias"); - b.Property("HasVanity") + b.Property("HasVanity") .HasColumnType("boolean") .HasDefaultValue(false) .HasColumnName("has_vanity"); diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs index 0859d55..fbc7b07 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs @@ -14,7 +14,7 @@ namespace LightlessSyncServer.Migrations name: "has_vanity", table: "users", type: "boolean", - nullable: true, + nullable: false, defaultValue: false); migrationBuilder.AddColumn( diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index 728a7c8..751cfd6 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -683,7 +683,7 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(15)") .HasColumnName("alias"); - b.Property("HasVanity") + b.Property("HasVanity") .HasColumnType("boolean") .HasDefaultValue(false) .HasColumnName("has_vanity"); diff --git a/LightlessSyncServer/LightlessSyncShared/Models/User.cs b/LightlessSyncServer/LightlessSyncShared/Models/User.cs index f36fc84..6e7f612 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/User.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/User.cs @@ -14,7 +14,7 @@ public class User public bool IsAdmin { get; set; } = false; - public bool? HasVanity { get; set; } = false; + public bool HasVanity { get; set; } = false; [MaxLength(9)] public string? TextColorHex { get; set; } = string.Empty; From b9abdcfff718fd14b6df8efb34f5105bee9be38f Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 22:36:51 +0900 Subject: [PATCH 28/40] update migrations --- ...igner.cs => 20250926133055_AddUserVanity.Designer.cs} | 9 +++------ ...erVanityFields.cs => 20250926133055_AddUserVanity.cs} | 8 +++----- .../Migrations/LightlessDbContextModelSnapshot.cs | 5 +---- 3 files changed, 7 insertions(+), 15 deletions(-) rename LightlessSyncServer/LightlessSyncShared/Migrations/{20250925225205_AddUserVanityFields.Designer.cs => 20250926133055_AddUserVanity.Designer.cs} (99%) rename LightlessSyncServer/LightlessSyncShared/Migrations/{20250925225205_AddUserVanityFields.cs => 20250926133055_AddUserVanity.cs} (87%) diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250926133055_AddUserVanity.Designer.cs similarity index 99% rename from LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs rename to LightlessSyncServer/LightlessSyncShared/Migrations/20250926133055_AddUserVanity.Designer.cs index c667a50..c2956de 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.Designer.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250926133055_AddUserVanity.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using LightlessSyncShared.Data; using Microsoft.EntityFrameworkCore; @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LightlessSyncServer.Migrations { [DbContext(typeof(LightlessDbContext))] - [Migration("20250925225205_AddUserVanityFields")] - partial class AddUserVanityFields + [Migration("20250926133055_AddUserVanity")] + partial class AddUserVanity { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -688,7 +688,6 @@ namespace LightlessSyncServer.Migrations b.Property("HasVanity") .HasColumnType("boolean") - .HasDefaultValue(false) .HasColumnName("has_vanity"); b.Property("IsAdmin") @@ -706,13 +705,11 @@ namespace LightlessSyncServer.Migrations b.Property("TextColorHex") .HasMaxLength(9) .HasColumnType("character varying(9)") - .HasDefaultValue(string.Empty) .HasColumnName("text_color_hex"); b.Property("TextGlowColorHex") .HasMaxLength(9) .HasColumnType("character varying(9)") - .HasDefaultValue(string.Empty) .HasColumnName("text_glow_color_hex"); b.Property("Timestamp") diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20250926133055_AddUserVanity.cs similarity index 87% rename from LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs rename to LightlessSyncServer/LightlessSyncShared/Migrations/20250926133055_AddUserVanity.cs index fbc7b07..714c6e1 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20250925225205_AddUserVanityFields.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20250926133055_AddUserVanity.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace LightlessSyncServer.Migrations { /// - public partial class AddUserVanityFields : Migration + public partial class AddUserVanity : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -22,16 +22,14 @@ namespace LightlessSyncServer.Migrations table: "users", type: "character varying(9)", maxLength: 9, - nullable: true, - defaultValue: ""); + nullable: true); migrationBuilder.AddColumn( name: "text_glow_color_hex", table: "users", type: "character varying(9)", maxLength: 9, - nullable: true, - defaultValue: ""); + nullable: true); } /// diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index 751cfd6..578c910 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using LightlessSyncShared.Data; using Microsoft.EntityFrameworkCore; @@ -685,7 +685,6 @@ namespace LightlessSyncServer.Migrations b.Property("HasVanity") .HasColumnType("boolean") - .HasDefaultValue(false) .HasColumnName("has_vanity"); b.Property("IsAdmin") @@ -703,13 +702,11 @@ namespace LightlessSyncServer.Migrations b.Property("TextColorHex") .HasMaxLength(9) .HasColumnType("character varying(9)") - .HasDefaultValue(string.Empty) .HasColumnName("text_color_hex"); b.Property("TextGlowColorHex") .HasMaxLength(9) .HasColumnType("character varying(9)") - .HasDefaultValue(string.Empty) .HasColumnName("text_glow_color_hex"); b.Property("Timestamp") From 6bc9da151971ab3bd7eb51e90d512af39f5f6dfd Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 26 Sep 2025 23:41:17 +0900 Subject: [PATCH 29/40] adjust connection method --- LightlessAPI | 2 +- LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index b85b54f..5bfd21a 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit b85b54f560d3d4d901b3af1421337a4b22b0d067 +Subproject commit 5bfd21aaa90817f14c9e2931e77b20f4276f16ed diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs index 17ca1e3..901a263 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs @@ -111,6 +111,9 @@ public partial class LightlessHub : Hub, ILightlessHub ServerVersion = ILightlessHub.ApiVersion, IsAdmin = dbUser.IsAdmin, IsModerator = dbUser.IsModerator, + HasVanity = dbUser.HasVanity, + TextColorHex = dbUser.TextColorHex, + TextGlowColorHex = dbUser.TextGlowColorHex, ServerInfo = new ServerInfo() { MaxGroupsCreatedByUser = _maxExistingGroupsByUser, From ed13ee8921c785f01f717e1242486dcc3ce9bf5f Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:31:58 +0900 Subject: [PATCH 30/40] lightfinder config, securing methods with stricter checking and added pair request notifications --- LightlessAPI | 2 +- .../Configuration/BroadcastConfiguration.cs | 74 ++++++++ .../Configuration/BroadcastOptions.cs | 23 +++ .../Configuration/IBroadcastConfiguration.cs | 16 ++ .../Hubs/LightlessHub.Groups.cs | 25 ++- .../Hubs/LightlessHub.User.cs | 172 ++++++++++++++++-- .../LightlessSyncServer/Hubs/LightlessHub.cs | 7 +- .../LightlessSyncServer/Startup.cs | 3 + .../LightlessSyncServer/appsettings.json | 9 + 9 files changed, 311 insertions(+), 20 deletions(-) create mode 100644 LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs create mode 100644 LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs diff --git a/LightlessAPI b/LightlessAPI index 5bfd21a..69f0e31 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 5bfd21aaa90817f14c9e2931e77b20f4276f16ed +Subproject commit 69f0e310bd78e0c56eab298199e6e2ca15bf56bd diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs new file mode 100644 index 0000000..5b796aa --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.Extensions.Options; + +namespace LightlessSyncServer.Configuration; + +public class BroadcastConfiguration : IBroadcastConfiguration +{ + private static readonly TimeSpan DefaultEntryTtl = TimeSpan.FromMinutes(5); + private const int DefaultMaxStatusBatchSize = 30; + private const string DefaultNotificationTemplate = "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back."; + + private readonly IOptionsMonitor _optionsMonitor; + + public BroadcastConfiguration(IOptionsMonitor optionsMonitor) + { + _optionsMonitor = optionsMonitor; + } + + private BroadcastOptions Options => _optionsMonitor.CurrentValue ?? new BroadcastOptions(); + + public string RedisKeyPrefix + { + get + { + var prefix = Options.RedisKeyPrefix; + return string.IsNullOrWhiteSpace(prefix) ? "broadcast:" : prefix!; + } + } + + public TimeSpan BroadcastEntryTtl + { + get + { + var seconds = Options.EntryTtlSeconds; + return seconds > 0 ? TimeSpan.FromSeconds(seconds) : DefaultEntryTtl; + } + } + + public int MaxStatusBatchSize + { + get + { + var value = Options.MaxStatusBatchSize; + return value > 0 ? value : DefaultMaxStatusBatchSize; + } + } + + public bool NotifyOwnerOnPairRequest => Options.NotifyOwnerOnPairRequest; + + public bool EnableBroadcasting => Options.EnableBroadcasting; + + public bool EnableSyncshellBroadcastPayloads => Options.EnableSyncshellBroadcastPayloads; + + public string BuildRedisKey(string hashedCid) + { + if (string.IsNullOrEmpty(hashedCid)) + return RedisKeyPrefix; + + return string.Concat(RedisKeyPrefix, hashedCid); + } + + public string BuildPairRequestNotification(string displayName) + { + var template = Options.PairRequestNotificationTemplate; + if (string.IsNullOrWhiteSpace(template)) + { + template = DefaultNotificationTemplate; + } + + displayName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName; + + return template.Replace("{DisplayName}", displayName, StringComparison.Ordinal); + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs new file mode 100644 index 0000000..960c8ca --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace LightlessSyncServer.Configuration; + +public class BroadcastOptions +{ + [Required] + public string RedisKeyPrefix { get; set; } = "broadcast:"; + + [Range(1, int.MaxValue)] + public int EntryTtlSeconds { get; set; } = 300; + + [Range(1, int.MaxValue)] + public int MaxStatusBatchSize { get; set; } = 30; + + public bool NotifyOwnerOnPairRequest { get; set; } = true; + + public bool EnableBroadcasting { get; set; } = true; + + public bool EnableSyncshellBroadcastPayloads { get; set; } = true; + + public string PairRequestNotificationTemplate { get; set; } = "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back."; +} diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs new file mode 100644 index 0000000..7320720 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs @@ -0,0 +1,16 @@ +using System; + +namespace LightlessSyncServer.Configuration; + +public interface IBroadcastConfiguration +{ + string RedisKeyPrefix { get; } + TimeSpan BroadcastEntryTtl { get; } + int MaxStatusBatchSize { get; } + bool NotifyOwnerOnPairRequest { get; } + bool EnableBroadcasting { get; } + bool EnableSyncshellBroadcastPayloads { get; } + + string BuildRedisKey(string hashedCid); + string BuildPairRequestNotification(string displayName); +} diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 9713831..1204590 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -1,4 +1,4 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; @@ -925,6 +925,13 @@ public partial class LightlessHub return false; } + if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.EnableSyncshellBroadcastPayloads) + { + _logger.LogCallWarning(LightlessHubLogger.Args("syncshell broadcast disabled", "User", UserUID, "GID", dto.GID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Syncshell broadcasting is currently disabled.").ConfigureAwait(false); + return false; + } + var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false); if (!isOwner) { @@ -941,6 +948,9 @@ public partial class LightlessHub { _logger.LogCallInfo(LightlessHubLogger.Args("Requested Syncshells", broadcastEntries.Select(b => b.GID))); + if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.EnableSyncshellBroadcastPayloads) + return new List(); + var results = new List(); var gidsToValidate = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -949,10 +959,19 @@ public partial class LightlessHub if (string.IsNullOrWhiteSpace(entry.HashedCID) || string.IsNullOrWhiteSpace(entry.GID)) continue; - var redisKey = $"broadcast:{entry.HashedCID}"; + var redisKey = _broadcastConfiguration.BuildRedisKey(entry.HashedCID); var redisEntry = await _redis.GetAsync(redisKey).ConfigureAwait(false); - if (redisEntry?.GID != null && string.Equals(redisEntry.GID, entry.GID, StringComparison.OrdinalIgnoreCase)) + if (redisEntry is null) + continue; + + if (!string.IsNullOrEmpty(redisEntry.HashedCID) && !string.Equals(redisEntry.HashedCID, entry.HashedCID, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("mismatched broadcast cid for group lookup", "Requested", entry.HashedCID, "EntryCID", redisEntry.HashedCID)); + continue; + } + + if (redisEntry.GID != null && string.Equals(redisEntry.GID, entry.GID, StringComparison.OrdinalIgnoreCase)) gidsToValidate.Add(entry.GID); } diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index ddd0b1b..3248a00 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -4,14 +4,17 @@ using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; +using LightlessSyncServer.Configuration; using LightlessSyncShared.Metrics; using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using StackExchange.Redis; +using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -252,9 +255,62 @@ public partial class LightlessHub await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("stored pairing request", myCid, otherCid)); + await NotifyBroadcastOwnerOfPairRequest(otherCid).ConfigureAwait(false); } } + + private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid) + { + if (string.IsNullOrWhiteSpace(targetHashedCid)) + return; + + if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest) + return; + + var db = _redis.Database; + var broadcastKey = _broadcastConfiguration.BuildRedisKey(targetHashedCid); + RedisValueWithExpiry broadcastValue; + + try + { + broadcastValue = await db.StringGetWithExpiryAsync(broadcastKey).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to fetch broadcast for pair notify", "CID", targetHashedCid, "Error", ex)); + return; + } + + if (broadcastValue.Value.IsNullOrEmpty || broadcastValue.Expiry is null || broadcastValue.Expiry <= TimeSpan.Zero) + return; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(broadcastValue.Value!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast for pair notify", "CID", targetHashedCid, "Value", broadcastValue.Value, "Error", ex)); + return; + } + + if (entry is null || !string.Equals(entry.HashedCID, targetHashedCid, StringComparison.Ordinal)) + return; + + if (!entry.HasOwner()) + return; + + if (string.Equals(entry.OwnerUID, UserUID, StringComparison.Ordinal)) + return; + + var senderAlias = Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Alias, StringComparison.Ordinal))?.Value; + var displayName = string.IsNullOrWhiteSpace(senderAlias) ? UserUID : senderAlias; + var message = _broadcastConfiguration.BuildPairRequestNotification(displayName); + + await Clients.User(entry.OwnerUID).Client_ReceiveServerMessage(MessageSeverity.Information, message).ConfigureAwait(false); + } private class PairingPayload { public string UID { get; set; } = string.Empty; @@ -264,14 +320,26 @@ public partial class LightlessHub public class BroadcastRedisEntry { public string HashedCID { get; set; } = string.Empty; + public string OwnerUID { get; set; } = string.Empty; public string? GID { get; set; } + + public bool OwnedBy(string userUid) => !string.IsNullOrEmpty(userUid) && string.Equals(OwnerUID, userUid, StringComparison.Ordinal); + + public bool HasOwner() => !string.IsNullOrEmpty(OwnerUID); } [Authorize(Policy = "Identified")] public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) { + if (enabled && !_broadcastConfiguration.EnableBroadcasting) + { + _logger.LogCallWarning(LightlessHubLogger.Args("broadcast disabled", UserUID, "CID", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Broadcasting is currently disabled.").ConfigureAwait(false); + return; + } + var db = _redis.Database; - var broadcastKey = $"broadcast:{hashedCid}"; + var broadcastKey = _broadcastConfiguration.BuildRedisKey(hashedCid); if (enabled) { @@ -279,6 +347,13 @@ public partial class LightlessHub if (groupDto is not null) { + if (!_broadcastConfiguration.EnableSyncshellBroadcastPayloads) + { + _logger.LogCallWarning(LightlessHubLogger.Args("syncshell broadcast disabled", UserUID, "CID", hashedCid, "GID", groupDto.GID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Syncshell broadcasting is currently disabled.").ConfigureAwait(false); + return; + } + groupDto.HashedCID = hashedCid; var valid = await SetGroupBroadcastStatus(groupDto).ConfigureAwait(false); @@ -288,14 +363,36 @@ public partial class LightlessHub gid = groupDto.GID; } + BroadcastRedisEntry? existingEntry = null; + var existingValue = await db.StringGetAsync(broadcastKey).ConfigureAwait(false); + if (!existingValue.IsNullOrEmpty) + { + try + { + existingEntry = JsonSerializer.Deserialize(existingValue!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast entry during enable", "CID", hashedCid, "Value", existingValue, "Error", ex)); + } + + if (existingEntry is not null && existingEntry.HasOwner() && !existingEntry.OwnedBy(UserUID)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to take broadcast ownership", UserUID, "CID", hashedCid, "ExistingOwner", existingEntry.OwnerUID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Another user is already broadcasting with that CID."); + return; + } + } + var entry = new BroadcastRedisEntry { HashedCID = hashedCid, + OwnerUID = UserUID, GID = gid, }; var json = JsonSerializer.Serialize(entry); - await db.StringSetAsync(broadcastKey, json, TimeSpan.FromMinutes(5)).ConfigureAwait(false); + await db.StringSetAsync(broadcastKey, json, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid)); } else @@ -315,13 +412,20 @@ public partial class LightlessHub return; } - if (entry is null || entry.HashedCID != hashedCid) + if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Stored", entry?.HashedCID)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); return; } + if (entry.HasOwner() && !entry.OwnedBy(UserUID)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + return; + } + await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID)); } @@ -331,8 +435,11 @@ public partial class LightlessHub [Authorize(Policy = "Identified")] public async Task IsUserBroadcasting(string hashedCid) { + if (!_broadcastConfiguration.EnableBroadcasting) + return null; + var db = _redis.Database; - var key = $"broadcast:{hashedCid}"; + var key = _broadcastConfiguration.BuildRedisKey(hashedCid); var result = await db.StringGetWithExpiryAsync(key).ConfigureAwait(false); if (result.Expiry is null || result.Expiry <= TimeSpan.Zero || result.Value.IsNullOrEmpty) @@ -348,6 +455,12 @@ public partial class LightlessHub return null; } + if (entry is not null && !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("mismatched broadcast entry", "CID", hashedCid, "EntryCID", entry.HashedCID)); + return null; + } + var dto = new BroadcastStatusInfoDto { HashedCID = entry?.HashedCID ?? hashedCid, @@ -363,8 +476,11 @@ public partial class LightlessHub [Authorize(Policy = "Identified")] public async Task GetBroadcastTtl(string hashedCid) { + if (!_broadcastConfiguration.EnableBroadcasting) + return null; + var db = _redis.Database; - var key = $"broadcast:{hashedCid}"; + var key = _broadcastConfiguration.BuildRedisKey(hashedCid); var value = await db.StringGetAsync(key).ConfigureAwait(false); if (value.IsNullOrEmpty) @@ -381,9 +497,21 @@ public partial class LightlessHub return null; } - if (entry?.HashedCID != hashedCid) + if (entry is null) { - _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "EntryCID", entry?.HashedCID)); + _logger.LogCallWarning(LightlessHubLogger.Args("missing broadcast entry during ttl query", "CID", hashedCid)); + return null; + } + + if (!string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "EntryCID", entry.HashedCID)); + return null; + } + + if (entry.HasOwner() && !entry.OwnedBy(UserUID)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID)); return null; } @@ -396,19 +524,25 @@ public partial class LightlessHub } - private const int MaxBatchSize = 30; - [Authorize(Policy = "Identified")] - public async Task AreUsersBroadcasting(List hashedCids) + public async Task AreUsersBroadcasting(List hashedCids) { + if (!_broadcastConfiguration.EnableBroadcasting) + { + _logger.LogCallInfo(LightlessHubLogger.Args("batch broadcast disabled", "Count", hashedCids.Count)); + return null; + } + + var maxBatchSize = _broadcastConfiguration.MaxStatusBatchSize; + if (hashedCids.Count > maxBatchSize) + hashedCids = hashedCids.Take(maxBatchSize).ToList(); + var db = _redis.Database; - if (hashedCids.Count > MaxBatchSize) - hashedCids = hashedCids.Take(MaxBatchSize).ToList(); var tasks = new Dictionary>(hashedCids.Count); foreach (var cid in hashedCids) { - var key = $"broadcast:{cid}"; + var key = _broadcastConfiguration.BuildRedisKey(cid); tasks[cid] = db.StringGetWithExpiryAsync(key); } @@ -433,7 +567,17 @@ public partial class LightlessHub try { entry = JsonSerializer.Deserialize(raw!); - gid = entry?.GID; + if (entry is not null && !string.Equals(entry.HashedCID, cid, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("mismatched broadcast cid in batch", "Requested", cid, "EntryCID", entry.HashedCID)); + entry = null; + gid = null; + isBroadcasting = false; + } + else + { + gid = entry?.GID; + } } catch (Exception ex) { diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs index 901a263..402e8be 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs @@ -1,8 +1,9 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto; using LightlessSync.API.SignalR; using LightlessSyncServer.Services; +using LightlessSyncServer.Configuration; using LightlessSyncServer.Utils; using LightlessSyncShared; using LightlessSyncShared.Data; @@ -29,6 +30,7 @@ public partial class LightlessHub : Hub, ILightlessHub private readonly LightlessHubLogger _logger; private readonly string _shardName; private readonly int _maxExistingGroupsByUser; + private readonly IBroadcastConfiguration _broadcastConfiguration; private readonly int _maxJoinedGroupsByUser; private readonly int _maxGroupUserCount; private readonly IRedisDatabase _redis; @@ -46,7 +48,7 @@ public partial class LightlessHub : Hub, ILightlessHub IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, IConfigurationService configuration, IHttpContextAccessor contextAccessor, IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus, - GPoseLobbyDistributionService gPoseLobbyDistributionService, PairService pairService) + GPoseLobbyDistributionService gPoseLobbyDistributionService, IBroadcastConfiguration broadcastConfiguration, PairService pairService) { _lightlessMetrics = lightlessMetrics; _systemInfoService = systemInfoService; @@ -65,6 +67,7 @@ public partial class LightlessHub : Hub, ILightlessHub _gPoseLobbyDistributionService = gPoseLobbyDistributionService; _logger = new LightlessHubLogger(this, logger); _dbContextLazy = new Lazy(() => lightlessDbContextFactory.CreateDbContext()); + _broadcastConfiguration = broadcastConfiguration; _pairService = pairService; } diff --git a/LightlessSyncServer/LightlessSyncServer/Startup.cs b/LightlessSyncServer/LightlessSyncServer/Startup.cs index 5b0b672..b647e85 100644 --- a/LightlessSyncServer/LightlessSyncServer/Startup.cs +++ b/LightlessSyncServer/LightlessSyncServer/Startup.cs @@ -2,6 +2,7 @@ using AspNetCoreRateLimit; using LightlessSync.API.SignalR; using LightlessSyncAuthService.Controllers; using LightlessSyncServer.Controllers; +using LightlessSyncServer.Configuration; using LightlessSyncServer.Hubs; using LightlessSyncServer.Services; using LightlessSyncShared.Data; @@ -87,7 +88,9 @@ public class Startup services.Configure(Configuration.GetRequiredSection("LightlessSync")); services.Configure(Configuration.GetRequiredSection("LightlessSync")); + services.Configure(Configuration.GetSection("Broadcast")); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LightlessSyncServer/LightlessSyncServer/appsettings.json b/LightlessSyncServer/LightlessSyncServer/appsettings.json index 880bf73..80524cb 100644 --- a/LightlessSyncServer/LightlessSyncServer/appsettings.json +++ b/LightlessSyncServer/LightlessSyncServer/appsettings.json @@ -29,6 +29,15 @@ "ServiceAddress": "http://localhost:5002", "StaticFileServiceAddress": "http://localhost:5003" }, + "Broadcast": { + "RedisKeyPrefix": "broadcast:", + "EntryTtlSeconds": 10800, + "MaxStatusBatchSize": 30, + "NotifyOwnerOnPairRequest": true, + "EnableBroadcasting": true, + "EnableSyncshellBroadcastPayloads": true, + "PairRequestNotificationTemplate": "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back." + }, "AllowedHosts": "*", "Kestrel": { "Endpoints": { From d2dabddeb794ae2c1394e964099108701853007d Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:41:56 +0900 Subject: [PATCH 31/40] validate incoming cid's --- .../Configuration/BroadcastConfiguration.cs | 2 +- .../Configuration/BroadcastOptions.cs | 2 +- .../Hubs/LightlessHub.Groups.cs | 7 +-- .../Hubs/LightlessHub.User.cs | 51 +++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs index 5b796aa..560ea00 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs @@ -5,7 +5,7 @@ namespace LightlessSyncServer.Configuration; public class BroadcastConfiguration : IBroadcastConfiguration { - private static readonly TimeSpan DefaultEntryTtl = TimeSpan.FromMinutes(5); + private static readonly TimeSpan DefaultEntryTtl = TimeSpan.FromMinutes(180); private const int DefaultMaxStatusBatchSize = 30; private const string DefaultNotificationTemplate = "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back."; diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs index 960c8ca..858255a 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs @@ -8,7 +8,7 @@ public class BroadcastOptions public string RedisKeyPrefix { get; set; } = "broadcast:"; [Range(1, int.MaxValue)] - public int EntryTtlSeconds { get; set; } = 300; + public int EntryTtlSeconds { get; set; } = 10800; [Range(1, int.MaxValue)] public int MaxStatusBatchSize { get; set; } = 30; diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 1204590..69a32e4 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -371,11 +371,6 @@ public partial class LightlessHub return new GroupJoinInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), true); } - private static bool IsHex(char c) => - (c >= '0' && c <= '9') || - (c >= 'a' && c <= 'f') || - (c >= 'A' && c <= 'F'); - [Authorize(Policy = "Identified")] public async Task GroupJoinFinalize(GroupJoinDto dto) { @@ -386,7 +381,7 @@ public partial class LightlessHub var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); - var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(IsHex); + var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(Uri.IsHexDigit); var hashedPw = isHashedPassword ? dto.Password : StringUtils.Sha256String(dto.Password); diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index 3248a00..914e181 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -148,6 +148,13 @@ public partial class LightlessHub if (string.IsNullOrWhiteSpace(otherCid) || string.IsNullOrWhiteSpace(myCid)) return; + bool IsValidCid(string cid) => cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); + + if (!IsValidCid(myCid) || !IsValidCid(otherCid)) + { + return; + } + if (string.Equals(otherCid, myCid, StringComparison.Ordinal)) { await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You can't pair with yourself.").ConfigureAwait(false); @@ -338,6 +345,18 @@ public partial class LightlessHub return; } + if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); + return; + } + + if (hashedCid.All(c => c == '0')) + { + return; + } + var db = _redis.Database; var broadcastKey = _broadcastConfiguration.BuildRedisKey(hashedCid); @@ -438,6 +457,18 @@ public partial class LightlessHub if (!_broadcastConfiguration.EnableBroadcasting) return null; + if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); + return null; + } + + if (hashedCid.All(c => c == '0')) + { + return null; + } + var db = _redis.Database; var key = _broadcastConfiguration.BuildRedisKey(hashedCid); @@ -479,6 +510,18 @@ public partial class LightlessHub if (!_broadcastConfiguration.EnableBroadcasting) return null; + if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + { + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); + return null; + } + + if (hashedCid.All(c => c == '0')) + { + return null; + } + var db = _redis.Database; var key = _broadcastConfiguration.BuildRedisKey(hashedCid); @@ -542,6 +585,14 @@ public partial class LightlessHub var tasks = new Dictionary>(hashedCids.Count); foreach (var cid in hashedCids) { + bool validHash = !string.IsNullOrWhiteSpace(cid) && cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); + + if (!validHash) + { + tasks[cid] = Task.FromResult(new RedisValueWithExpiry(RedisValue.Null, null)); + continue; + } + var key = _broadcastConfiguration.BuildRedisKey(cid); tasks[cid] = db.StringGetWithExpiryAsync(key); } From 610461fa995acb3afacbfa3ce7ae01d4626c4600 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:21:21 +0900 Subject: [PATCH 32/40] adjusting notifications --- LightlessAPI | 2 +- .../Configuration/BroadcastConfiguration.cs | 6 ++---- .../Configuration/IBroadcastConfiguration.cs | 2 +- .../Hubs/LightlessHub.ClientStubs.cs | 1 + .../LightlessSyncServer/Hubs/LightlessHub.User.cs | 14 ++++++++------ 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 69f0e31..6c542c0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 69f0e310bd78e0c56eab298199e6e2ca15bf56bd +Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs index 560ea00..893ec65 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs @@ -59,7 +59,7 @@ public class BroadcastConfiguration : IBroadcastConfiguration return string.Concat(RedisKeyPrefix, hashedCid); } - public string BuildPairRequestNotification(string displayName) + public string BuildPairRequestNotification() { var template = Options.PairRequestNotificationTemplate; if (string.IsNullOrWhiteSpace(template)) @@ -67,8 +67,6 @@ public class BroadcastConfiguration : IBroadcastConfiguration template = DefaultNotificationTemplate; } - displayName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName; - - return template.Replace("{DisplayName}", displayName, StringComparison.Ordinal); + return template; } } diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs index 7320720..0f741f7 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs @@ -12,5 +12,5 @@ public interface IBroadcastConfiguration bool EnableSyncshellBroadcastPayloads { get; } string BuildRedisKey(string hashedCid); - string BuildPairRequestNotification(string displayName); + string BuildPairRequestNotification(); } diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.ClientStubs.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.ClientStubs.cs index c4fbe0c..7fbb954 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.ClientStubs.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.ClientStubs.cs @@ -19,6 +19,7 @@ namespace LightlessSyncServer.Hubs public Task Client_GroupSendProfile(GroupProfileDto groupProfile) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_GroupSendInfo(GroupInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_UserAddClientPair(UserPairDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index 914e181..5a8c164 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -262,14 +262,14 @@ public partial class LightlessHub await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("stored pairing request", myCid, otherCid)); - await NotifyBroadcastOwnerOfPairRequest(otherCid).ConfigureAwait(false); + await NotifyBroadcastOwnerOfPairRequest(myCid, otherCid).ConfigureAwait(false); } } - private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid) + private async Task NotifyBroadcastOwnerOfPairRequest(string myHashedCid, string targetHashedCid) { - if (string.IsNullOrWhiteSpace(targetHashedCid)) + if (string.IsNullOrWhiteSpace(targetHashedCid) || string.IsNullOrWhiteSpace(myHashedCid)) return; if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest) @@ -313,10 +313,12 @@ public partial class LightlessHub return; var senderAlias = Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Alias, StringComparison.Ordinal))?.Value; - var displayName = string.IsNullOrWhiteSpace(senderAlias) ? UserUID : senderAlias; - var message = _broadcastConfiguration.BuildPairRequestNotification(displayName); + //var displayName = string.IsNullOrWhiteSpace(senderAlias) ? UserUID : senderAlias; + var message = _broadcastConfiguration.BuildPairRequestNotification(); - await Clients.User(entry.OwnerUID).Client_ReceiveServerMessage(MessageSeverity.Information, message).ConfigureAwait(false); + var dto = new UserPairNotificationDto{myHashedCid = myHashedCid, message = message}; + + await Clients.User(entry.OwnerUID).Client_ReceiveBroadcastPairRequest(dto).ConfigureAwait(false); } private class PairingPayload { From 1655f990211d5e352584c39976ad44a2ae5110d0 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 4 Oct 2025 13:51:25 -0500 Subject: [PATCH 33/40] Update Submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 6c542c0..167508d 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 +Subproject commit 167508d27b754347554797fa769c5feb3f91552e From 43219dd1e97c9a01c1195129cbce07c4bc1f4912 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 4 Oct 2025 14:09:32 -0500 Subject: [PATCH 34/40] Allow kdb --- .../LightlessSyncServer/Hubs/LightlessHub.User.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index 5a8c164..d9ba6eb 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -23,7 +23,7 @@ namespace LightlessSyncServer.Hubs; public partial class LightlessHub { - private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk" }; + private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb" }; [Authorize(Policy = "Identified")] public async Task UserAddPair(UserDto dto) From 3d9fc4fba03e5464da37c88ad40babf71b124af5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 5 Oct 2025 19:05:32 +0200 Subject: [PATCH 35/40] Removal of cancellation tokens --- .../Hubs/LightlessHub.Groups.cs | 61 ++++++++----------- .../Hubs/LightlessHub.User.cs | 21 +++---- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 69a32e4..e3ad30d 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -137,7 +137,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync().ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) @@ -181,7 +181,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync().ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); @@ -199,15 +199,15 @@ public partial class LightlessHub public async Task GroupCreate() { _logger.LogCallInfo(); - var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false); + var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false); if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) { throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); } var gid = StringUtils.GenerateRandomString(12); - while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) + while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid).ConfigureAwait(false)) { gid = StringUtils.GenerateRandomString(12); } @@ -218,7 +218,7 @@ public partial class LightlessHub var hashedPw = StringUtils.Sha256String(passwd); var currentTime = DateTime.UtcNow; - UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID).ConfigureAwait(false); Group newGroup = new() { @@ -255,7 +255,7 @@ public partial class LightlessHub await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal))) @@ -314,7 +314,7 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); - var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false); DbContext.RemoveRange(groupPairs); DbContext.Remove(group); await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); @@ -332,7 +332,7 @@ public partial class LightlessHub var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false); if (!userHasRights) return []; - var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync().ConfigureAwait(false); List bannedGroupUsers = banEntries.Select(b => new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn, @@ -350,14 +350,14 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; - var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); var hashedPw = StringUtils.Sha256String(dto.Password); - var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false); if (group == null || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) @@ -378,7 +378,7 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(Uri.IsHexDigit); @@ -416,7 +416,7 @@ public partial class LightlessHub FromFinder = isHashedPassword }; - var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID).ConfigureAwait(false); if (preferredPermissions == null) { GroupPairPreferredPermission newPerms = new() @@ -447,7 +447,7 @@ public partial class LightlessHub await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync().ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false); @@ -693,7 +693,7 @@ public partial class LightlessHub .Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); return usersToPrune.Count(); } @@ -717,15 +717,15 @@ public partial class LightlessHub var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList(); await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync().ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); if (userIdent == null) { - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); return; } @@ -743,10 +743,8 @@ public partial class LightlessHub { _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var cancellationToken = _contextAccessor.HttpContext.RequestAborted; - var data = await DbContext.GroupProfiles - .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken) + .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID) .ConfigureAwait(false); var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null); @@ -779,8 +777,7 @@ public partial class LightlessHub if (!hasRights) return; var groupProfileDb = await DbContext.GroupProfiles - .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, - _contextAccessor.HttpContext.RequestAborted) + .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID) .ConfigureAwait(false); if (groupProfileDb != null) @@ -838,7 +835,7 @@ public partial class LightlessHub await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false); } @@ -847,8 +844,6 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var ct = _contextAccessor.HttpContext.RequestAborted; - var result = await ( from gp in DbContext.GroupPairs .Include(gp => gp.Group) @@ -866,7 +861,7 @@ public partial class LightlessHub .ToList(), }) .AsNoTracking() - .ToListAsync(ct) + .ToListAsync() .ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(result)); @@ -899,7 +894,7 @@ public partial class LightlessHub var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); if (!userHasRights) return; - var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID).ConfigureAwait(false); if (banEntry == null) return; DbContext.Remove(banEntry); @@ -990,6 +985,4 @@ public partial class LightlessHub return results; } - - } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index d9ba6eb..0316555 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -14,7 +14,6 @@ using Microsoft.EntityFrameworkCore; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using StackExchange.Redis; -using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -35,7 +34,7 @@ public partial class LightlessHub if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return; // grab other user, check if it exists and if a pair already exists - var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false); if (otherUser == null) { await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false); @@ -51,7 +50,7 @@ public partial class LightlessHub var existingEntry = await DbContext.ClientPairs.AsNoTracking() .FirstOrDefaultAsync(p => - p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + p.User.UID == UserUID && p.OtherUserUID == otherUser.UID).ConfigureAwait(false); if (existingEntry != null) { @@ -60,7 +59,7 @@ public partial class LightlessHub } // grab self create new client pair and save - var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); @@ -74,9 +73,9 @@ public partial class LightlessHub var existingData = await GetPairInfo(UserUID, otherUser.UID).ConfigureAwait(false); var permissions = existingData?.OwnPermissions; - if (permissions == null || !permissions.Sticky) + if (permissions == null || !permissions.Sticky) { - var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID).ConfigureAwait(false); permissions = new UserPermissionSet() { @@ -89,7 +88,7 @@ public partial class LightlessHub Sticky = true }; - var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID).ConfigureAwait(false); if (existingDbPerms == null) { await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false); @@ -657,7 +656,7 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); foreach (var user in secondaryUsers) { @@ -710,7 +709,7 @@ public partial class LightlessHub return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); } - var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID).ConfigureAwait(false); if (data == null) return new UserProfileDto(user.User, false, null, null, null); if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation"); @@ -867,7 +866,7 @@ public partial class LightlessHub // check if client pair even exists ClientPair callerPair = - await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false); if (callerPair == null) return; var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false); @@ -918,7 +917,7 @@ public partial class LightlessHub if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); - var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false); if (existingData?.FlaggedForReport ?? false) { From 479b80a5a02f0d58901a6b5ecca20971ed6c3de4 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:40:56 +0900 Subject: [PATCH 36/40] lightfinder changes: - removed all ability to provide your cid to the server through params, cid is gained from JWT claims - improved verification of who owns a cid, which includes locking a cid to a uid - locks and persisting entries of broadcasting are cleaned up on disconnection - method identification logic was rewritten to fit these changes --- LightlessAPI | 2 +- .../Configuration/BroadcastConfiguration.cs | 11 ++ .../Configuration/BroadcastOptions.cs | 6 + .../Configuration/IBroadcastConfiguration.cs | 4 + .../Hubs/LightlessHub.Functions.cs | 66 +++++++- .../Hubs/LightlessHub.Groups.cs | 81 +++++----- .../Hubs/LightlessHub.User.cs | 151 +++++++++++++----- .../LightlessSyncServer/Hubs/LightlessHub.cs | 5 + .../LightlessSyncServer/appsettings.json | 4 +- 9 files changed, 243 insertions(+), 87 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 167508d..f3c6064 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 167508d27b754347554797fa769c5feb3f91552e +Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs index 893ec65..d2a0549 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs @@ -59,6 +59,14 @@ public class BroadcastConfiguration : IBroadcastConfiguration return string.Concat(RedisKeyPrefix, hashedCid); } + public string BuildUserOwnershipKey(string userUid) + { + if (string.IsNullOrWhiteSpace(userUid)) + throw new ArgumentException("User UID must not be null or empty.", nameof(userUid)); + + return string.Concat(RedisKeyPrefix, "owner:", userUid); + } + public string BuildPairRequestNotification() { var template = Options.PairRequestNotificationTemplate; @@ -69,4 +77,7 @@ public class BroadcastConfiguration : IBroadcastConfiguration return template; } + + public int PairRequestRateLimit => Options.PairRequestRateLimit > 0 ? Options.PairRequestRateLimit : 5; + public int PairRequestRateWindow => Options.PairRequestRateWindow > 0 ? Options.PairRequestRateWindow : 60; } diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs index 858255a..ecfaf3a 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs @@ -20,4 +20,10 @@ public class BroadcastOptions public bool EnableSyncshellBroadcastPayloads { get; set; } = true; public string PairRequestNotificationTemplate { get; set; } = "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back."; + + [Range(1, int.MaxValue)] + public int PairRequestRateLimit { get; set; } = 5; + + [Range(1, int.MaxValue)] + public int PairRequestRateWindow { get; set; } = 60; } diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs index 0f741f7..3a38aa2 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs @@ -12,5 +12,9 @@ public interface IBroadcastConfiguration bool EnableSyncshellBroadcastPayloads { get; } string BuildRedisKey(string hashedCid); + string BuildUserOwnershipKey(string userUid); string BuildPairRequestNotification(); + + int PairRequestRateLimit { get; } + int PairRequestRateWindow { get; } } diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index 40ca003..b7b816d 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -1,11 +1,13 @@ -using LightlessSyncShared.Models; -using Microsoft.EntityFrameworkCore; -using LightlessSyncServer.Utils; -using LightlessSyncShared.Utils; using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; +using LightlessSyncServer.Utils; using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; +using System.Text.Json; using System.Threading; namespace LightlessSyncServer.Hubs; @@ -95,13 +97,18 @@ public partial class LightlessHub private async Task RemoveUserFromRedis() { + if (IsValidHashedCid(UserCharaIdent)) + { + await _redis.RemoveAsync("CID:" + UserCharaIdent, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } private async Task EnsureUserHasVanity(string uid, CancellationToken cancellationToken = default) { cancellationToken = cancellationToken == default && _contextAccessor.HttpContext != null - ? _contextAccessor.HttpContext.RequestAborted + ? RequestAbortedToken : cancellationToken; var user = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid, cancellationToken).ConfigureAwait(false); @@ -120,6 +127,47 @@ public partial class LightlessHub return user; } + private async Task ClearOwnedBroadcastLock() + { + var db = _redis.Database; + var ownershipKey = _broadcastConfiguration.BuildUserOwnershipKey(UserUID); + var ownedCidValue = await db.StringGetAsync(ownershipKey).ConfigureAwait(false); + if (ownedCidValue.IsNullOrEmpty) + return; + + var ownedCid = ownedCidValue.ToString(); + + await db.KeyDeleteAsync(ownershipKey, CommandFlags.FireAndForget).ConfigureAwait(false); + + if (string.IsNullOrEmpty(ownedCid)) + return; + + var broadcastKey = _broadcastConfiguration.BuildRedisKey(ownedCid); + var broadcastValue = await db.StringGetAsync(broadcastKey).ConfigureAwait(false); + if (broadcastValue.IsNullOrEmpty) + return; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(broadcastValue!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast during disconnect cleanup", "CID", ownedCid, "Value", broadcastValue, "Error", ex)); + return; + } + + if (entry is null) + return; + + if (entry.HasOwner() && !entry.OwnedBy(UserUID)) + return; + + await db.KeyDeleteAsync(broadcastKey, CommandFlags.FireAndForget).ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast cleaned on disconnect", UserUID, "CID", entry.HashedCID, "GID", entry.GID)); + } + private async Task SendGroupDeletedToAll(List groupUsers) { foreach (var pair in groupUsers) @@ -188,7 +236,13 @@ public partial class LightlessHub private async Task UpdateUserOnRedis() { - await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + var hashedCid = UserCharaIdent; + if (IsValidHashedCid(hashedCid)) + { + await _redis.AddAsync("CID:" + hashedCid, UserUID, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + + await _redis.AddAsync("UID:" + UserUID, hashedCid, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } private async Task UserGroupLeave(GroupPair groupUserPair, string userIdent, Dictionary allUserPairs, string? uid = null) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 69a32e4..30cb142 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -59,7 +59,7 @@ public partial class LightlessHub group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations); group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList(); await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false); @@ -137,7 +137,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) @@ -181,7 +181,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); @@ -199,15 +199,15 @@ public partial class LightlessHub public async Task GroupCreate() { _logger.LogCallInfo(); - var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) { throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); } var gid = StringUtils.GenerateRandomString(12); - while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) + while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid, cancellationToken: RequestAbortedToken).ConfigureAwait(false)) { gid = StringUtils.GenerateRandomString(12); } @@ -218,7 +218,7 @@ public partial class LightlessHub var hashedPw = StringUtils.Sha256String(passwd); var currentTime = DateTime.UtcNow; - UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); Group newGroup = new() { @@ -250,12 +250,12 @@ public partial class LightlessHub DisableVFX = defaultPermissions.DisableGroupAnimations, }; - await DbContext.Groups.AddAsync(newGroup, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - await DbContext.GroupPairs.AddAsync(initialPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.Groups.AddAsync(newGroup, RequestAbortedToken).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(initialPair, RequestAbortedToken).ConfigureAwait(false); + await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, RequestAbortedToken).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); - var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal))) @@ -314,10 +314,10 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); - var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.RemoveRange(groupPairs); DbContext.Remove(group); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); @@ -332,7 +332,7 @@ public partial class LightlessHub var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false); if (!userHasRights) return []; - var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); List bannedGroupUsers = banEntries.Select(b => new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn, @@ -350,14 +350,14 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; - var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var hashedPw = StringUtils.Sha256String(dto.Password); - var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (group == null || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) @@ -378,7 +378,7 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(Uri.IsHexDigit); @@ -416,7 +416,7 @@ public partial class LightlessHub FromFinder = isHashedPassword }; - var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (preferredPermissions == null) { GroupPairPreferredPermission newPerms = new() @@ -441,13 +441,13 @@ public partial class LightlessHub DbContext.Update(preferredPermissions); } - await DbContext.GroupPairs.AddAsync(newPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(newPair, RequestAbortedToken).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "Success")); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); - var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false); @@ -575,7 +575,7 @@ public partial class LightlessHub } } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); return true; } @@ -693,7 +693,7 @@ public partial class LightlessHub .Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); return usersToPrune.Count(); } @@ -717,15 +717,15 @@ public partial class LightlessHub var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList(); await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); if (userIdent == null) { - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); return; } @@ -743,7 +743,7 @@ public partial class LightlessHub { _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var cancellationToken = _contextAccessor.HttpContext.RequestAborted; + var cancellationToken = RequestAbortedToken; var data = await DbContext.GroupProfiles .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken) @@ -780,7 +780,7 @@ public partial class LightlessHub var groupProfileDb = await DbContext.GroupProfiles .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, - _contextAccessor.HttpContext.RequestAborted) + RequestAbortedToken) .ConfigureAwait(false); if (groupProfileDb != null) @@ -800,11 +800,11 @@ public partial class LightlessHub }; await DbContext.GroupProfiles.AddAsync(groupProfile, - _contextAccessor.HttpContext.RequestAborted) + RequestAbortedToken) .ConfigureAwait(false); } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); } [Authorize(Policy = "Identified")] @@ -836,9 +836,9 @@ public partial class LightlessHub userPair.IsModerator = false; } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); - var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false); } @@ -847,7 +847,7 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var ct = _contextAccessor.HttpContext.RequestAborted; + var ct = RequestAbortedToken; var result = await ( from gp in DbContext.GroupPairs @@ -899,11 +899,11 @@ public partial class LightlessHub var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); if (!userHasRights) return; - var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (banEntry == null) return; DbContext.Remove(banEntry); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); } @@ -991,5 +991,4 @@ public partial class LightlessHub return results; } - } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index d9ba6eb..ecd7025 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -1,4 +1,4 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; @@ -35,7 +35,7 @@ public partial class LightlessHub if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return; // grab other user, check if it exists and if a pair already exists - var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (otherUser == null) { await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false); @@ -51,7 +51,7 @@ public partial class LightlessHub var existingEntry = await DbContext.ClientPairs.AsNoTracking() .FirstOrDefaultAsync(p => - p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingEntry != null) { @@ -60,7 +60,7 @@ public partial class LightlessHub } // grab self create new client pair and save - var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); @@ -76,7 +76,7 @@ public partial class LightlessHub var permissions = existingData?.OwnPermissions; if (permissions == null || !permissions.Sticky) { - var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); permissions = new UserPermissionSet() { @@ -89,7 +89,7 @@ public partial class LightlessHub Sticky = true }; - var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingDbPerms == null) { await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false); @@ -143,17 +143,15 @@ public partial class LightlessHub } [Authorize(Policy = "Identified")] - public async Task TryPairWithContentId(string otherCid, string myCid) + public async Task TryPairWithContentId(string otherCid) { + var myCid = UserCharaIdent; + if (string.IsNullOrWhiteSpace(otherCid) || string.IsNullOrWhiteSpace(myCid)) return; - bool IsValidCid(string cid) => cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); - - if (!IsValidCid(myCid) || !IsValidCid(otherCid)) - { + if (!IsValidHashedCid(myCid) || !IsValidHashedCid(otherCid)) return; - } if (string.Equals(otherCid, myCid, StringComparison.Ordinal)) { @@ -181,12 +179,33 @@ public partial class LightlessHub try { var payload = JsonSerializer.Deserialize(json); - if (payload?.UID == null) + if (payload?.UID == null || string.IsNullOrWhiteSpace(payload.HashedCid)) { _logger.LogCallWarning(LightlessHubLogger.Args("invalid payload", reverseKey)); return; } + if (!IsValidHashedCid(payload.HashedCid) || !string.Equals(payload.HashedCid, otherCid, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("pairing cid mismatch", reverseKey, payload.HashedCid, otherCid)); + return; + } + + var expectedRequesterUid = await _redis.GetAsync("CID:" + payload.HashedCid).ConfigureAwait(false); + if (!string.Equals(expectedRequesterUid, payload.UID, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("pairing uid mismatch", reverseKey, payload.HashedCid, payload.UID, expectedRequesterUid ?? "null")); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Pair request could not be validated.").ConfigureAwait(false); + return; + } + + if (payload.Timestamp == default || DateTime.UtcNow - payload.Timestamp > TimeSpan.FromMinutes(5)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("stale pairing payload", reverseKey, payload.Timestamp)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Pair request expired.").ConfigureAwait(false); + return; + } + var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID); var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID); @@ -251,9 +270,30 @@ public partial class LightlessHub } else { + int maxRequests = _broadcastConfiguration.PairRequestRateLimit; + int requestWindow = _broadcastConfiguration.PairRequestRateWindow; + TimeSpan window = TimeSpan.FromSeconds(requestWindow); + var rateKey = $"pairing:limit:{UserUID}"; + var db = _redis.Database; + + var count = (long)await db.StringIncrementAsync(rateKey).ConfigureAwait(false); + if (count == 1) + { + await db.KeyExpireAsync(rateKey, window).ConfigureAwait(false); + } + + if (count > maxRequests) + { + var ttl = await db.KeyTimeToLiveAsync(rateKey).ConfigureAwait(false); + var secondsLeft = ttl?.TotalSeconds > 0 ? (int)ttl.Value.TotalSeconds : requestWindow; + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"You have exceeded the pair request limit. Please wait {secondsLeft} seconds before trying again.").ConfigureAwait(false); + return; + } + var payload = new PairingPayload { UID = UserUID, + HashedCid = myCid, Timestamp = DateTime.UtcNow }; @@ -262,14 +302,16 @@ public partial class LightlessHub await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("stored pairing request", myCid, otherCid)); - await NotifyBroadcastOwnerOfPairRequest(myCid, otherCid).ConfigureAwait(false); + await NotifyBroadcastOwnerOfPairRequest(otherCid).ConfigureAwait(false); } } - private async Task NotifyBroadcastOwnerOfPairRequest(string myHashedCid, string targetHashedCid) + private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid) { - if (string.IsNullOrWhiteSpace(targetHashedCid) || string.IsNullOrWhiteSpace(myHashedCid)) + var myHashedCid = UserCharaIdent; + + if (!IsValidHashedCid(targetHashedCid) || !IsValidHashedCid(myHashedCid)) return; if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest) @@ -323,6 +365,7 @@ public partial class LightlessHub private class PairingPayload { public string UID { get; set; } = string.Empty; + public string HashedCid { get; set; } = string.Empty; public DateTime Timestamp { get; set; } } @@ -338,8 +381,10 @@ public partial class LightlessHub } [Authorize(Policy = "Identified")] - public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) { + var hashedCid = UserCharaIdent; + if (enabled && !_broadcastConfiguration.EnableBroadcasting) { _logger.LogCallWarning(LightlessHubLogger.Args("broadcast disabled", UserUID, "CID", hashedCid)); @@ -347,9 +392,9 @@ public partial class LightlessHub return; } - if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + if (!IsValidHashedCid(hashedCid)) { - _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format for user ident", UserUID, "CID", hashedCid)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); return; } @@ -361,11 +406,32 @@ public partial class LightlessHub var db = _redis.Database; var broadcastKey = _broadcastConfiguration.BuildRedisKey(hashedCid); + var ownershipKey = _broadcastConfiguration.BuildUserOwnershipKey(UserUID); + var ownedCidValue = await db.StringGetAsync(ownershipKey).ConfigureAwait(false); + var ownedCid = ownedCidValue.IsNullOrEmpty ? null : ownedCidValue.ToString(); if (enabled) { string? gid = null; + if (!string.IsNullOrEmpty(ownedCid) && !string.Equals(ownedCid, hashedCid, StringComparison.Ordinal)) + { + var ownedBroadcastKey = _broadcastConfiguration.BuildRedisKey(ownedCid); + var ownedBroadcastValue = await db.StringGetAsync(ownedBroadcastKey).ConfigureAwait(false); + + if (ownedBroadcastValue.IsNullOrEmpty) + { + await db.KeyDeleteAsync(ownershipKey, CommandFlags.FireAndForget).ConfigureAwait(false); + ownedCid = null; + } + else + { + _logger.LogCallWarning(LightlessHubLogger.Args("multiple broadcast lock attempt", UserUID, "ExistingCID", ownedCid, "AttemptedCID", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You already have an active Lightfinder lock. Disable it before enabling another.").ConfigureAwait(false); + return; + } + } + if (groupDto is not null) { if (!_broadcastConfiguration.EnableSyncshellBroadcastPayloads) @@ -400,7 +466,7 @@ public partial class LightlessHub if (existingEntry is not null && existingEntry.HasOwner() && !existingEntry.OwnedBy(UserUID)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to take broadcast ownership", UserUID, "CID", hashedCid, "ExistingOwner", existingEntry.OwnerUID)); - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Another user is already broadcasting with that CID."); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Another user is already broadcasting with that CID.").ConfigureAwait(false); return; } } @@ -414,6 +480,7 @@ public partial class LightlessHub var json = JsonSerializer.Serialize(entry); await db.StringSetAsync(broadcastKey, json, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false); + await db.StringSetAsync(ownershipKey, hashedCid, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid)); } else @@ -436,22 +503,35 @@ public partial class LightlessHub if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Stored", entry?.HashedCID)); - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3").ConfigureAwait(false); return; } if (entry.HasOwner() && !entry.OwnedBy(UserUID)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID)); - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3").ConfigureAwait(false); return; } await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(ownedCid) && string.Equals(ownedCid, hashedCid, StringComparison.Ordinal)) + { + await db.KeyDeleteAsync(ownershipKey).ConfigureAwait(false); + } + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID)); } } + private static bool IsValidHashedCid(string? cid) + { + if (string.IsNullOrWhiteSpace(cid)) + return false; + + return cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); + } [Authorize(Policy = "Identified")] public async Task IsUserBroadcasting(string hashedCid) @@ -459,18 +539,13 @@ public partial class LightlessHub if (!_broadcastConfiguration.EnableBroadcasting) return null; - if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + if (!IsValidHashedCid(hashedCid)) { _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); return null; } - if (hashedCid.All(c => c == '0')) - { - return null; - } - var db = _redis.Database; var key = _broadcastConfiguration.BuildRedisKey(hashedCid); @@ -507,14 +582,16 @@ public partial class LightlessHub } [Authorize(Policy = "Identified")] - public async Task GetBroadcastTtl(string hashedCid) + public async Task GetBroadcastTtl() { if (!_broadcastConfiguration.EnableBroadcasting) return null; - if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + var hashedCid = UserCharaIdent; + + if (!IsValidHashedCid(hashedCid)) { - _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format for user ident", UserUID, "CID", hashedCid)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); return null; } @@ -587,9 +664,7 @@ public partial class LightlessHub var tasks = new Dictionary>(hashedCids.Count); foreach (var cid in hashedCids) { - bool validHash = !string.IsNullOrWhiteSpace(cid) && cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); - - if (!validHash) + if (!IsValidHashedCid(cid)) { tasks[cid] = Task.FromResult(new RedisValueWithExpiry(RedisValue.Null, null)); continue; @@ -657,7 +732,7 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); foreach (var user in secondaryUsers) { @@ -710,7 +785,7 @@ public partial class LightlessHub return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); } - var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (data == null) return new UserProfileDto(user.User, false, null, null, null); if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation"); @@ -867,7 +942,7 @@ public partial class LightlessHub // check if client pair even exists ClientPair callerPair = - await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (callerPair == null) return; var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false); @@ -918,7 +993,7 @@ public partial class LightlessHub if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); - var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingData?.FlaggedForReport ?? false) { diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs index 402e8be..318fc99 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis.Extensions.Core.Abstractions; using System.Collections.Concurrent; +using System.Threading; namespace LightlessSyncServer.Hubs; @@ -44,6 +45,8 @@ public partial class LightlessHub : Hub, ILightlessHub private readonly int _maxCharaDataByUser; private readonly int _maxCharaDataByUserVanity; + private CancellationToken RequestAbortedToken => _contextAccessor.HttpContext?.RequestAborted ?? Context?.ConnectionAborted ?? CancellationToken.None; + public LightlessHub(LightlessMetrics lightlessMetrics, IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, IConfigurationService configuration, IHttpContextAccessor contextAccessor, @@ -194,6 +197,8 @@ public partial class LightlessHub : Hub, ILightlessHub if (exception != null) _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace)); + await ClearOwnedBroadcastLock().ConfigureAwait(false); + await RemoveUserFromRedis().ConfigureAwait(false); _lightlessCensus.ClearStatistics(UserUID); diff --git a/LightlessSyncServer/LightlessSyncServer/appsettings.json b/LightlessSyncServer/LightlessSyncServer/appsettings.json index 80524cb..991b30a 100644 --- a/LightlessSyncServer/LightlessSyncServer/appsettings.json +++ b/LightlessSyncServer/LightlessSyncServer/appsettings.json @@ -36,7 +36,9 @@ "NotifyOwnerOnPairRequest": true, "EnableBroadcasting": true, "EnableSyncshellBroadcastPayloads": true, - "PairRequestNotificationTemplate": "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back." + "PairRequestNotificationTemplate": "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back.", + "PairRequestRateLimit": 5, + "PairRequestRateWindow": 60 }, "AllowedHosts": "*", "Kestrel": { From b6907a2704ae6aa369a28fdf5a47a81fc104ac93 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Fri, 10 Oct 2025 07:37:33 +0900 Subject: [PATCH 37/40] cdn downloads support --- LightlessAPI | 2 +- .../StaticFilesServerConfiguration.cs | 4 + .../Controllers/ServerFilesController.cs | 42 ++++++- .../Services/CDNDownloadUrlService.cs | 108 ++++++++++++++++++ .../LightlessSyncStaticFilesServer/Startup.cs | 1 + .../appsettings.json | 4 +- 6 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadUrlService.cs diff --git a/LightlessAPI b/LightlessAPI index f3c6064..89ac342 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 +Subproject commit 89ac34235d297655643684c425311cd68f0702f2 diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs index 85b9585..af4b3b6 100644 --- a/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncShared/Utils/Configuration/StaticFilesServerConfiguration.cs @@ -20,6 +20,8 @@ public class StaticFilesServerConfiguration : LightlessConfigurationBase public string ColdStorageDirectory { get; set; } = null; public double ColdStorageSizeHardLimitInGiB { get; set; } = -1; public int ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30; + public bool EnableDirectDownloads { get; set; } = true; + public int DirectDownloadTokenLifetimeSeconds { get; set; } = 300; [RemoteConfiguration] public double SpeedTestHoursRateLimit { get; set; } = 0.5; [RemoteConfiguration] @@ -40,6 +42,8 @@ public class StaticFilesServerConfiguration : LightlessConfigurationBase sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}"); sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}"); sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}"); + sb.AppendLine($"{nameof(EnableDirectDownloads)} => {EnableDirectDownloads}"); + sb.AppendLine($"{nameof(DirectDownloadTokenLifetimeSeconds)} => {DirectDownloadTokenLifetimeSeconds}"); return sb.ToString(); } } diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs index a81ca17..6ec924b 100644 --- a/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Controllers/ServerFilesController.cs @@ -10,6 +10,7 @@ using LightlessSyncShared.Services; using LightlessSyncShared.Utils.Configuration; using LightlessSyncStaticFilesServer.Services; using LightlessSyncStaticFilesServer.Utils; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -32,12 +33,13 @@ public class ServerFilesController : ControllerBase private readonly IDbContextFactory _lightlessDbContext; private readonly LightlessMetrics _metricsClient; private readonly MainServerShardRegistrationService _shardRegistrationService; + private readonly CDNDownloadUrlService _cdnDownloadUrlService; public ServerFilesController(ILogger logger, CachedFileProvider cachedFileProvider, IConfigurationService configuration, IHubContext hubContext, IDbContextFactory lightlessDbContext, LightlessMetrics metricsClient, - MainServerShardRegistrationService shardRegistrationService) : base(logger) + MainServerShardRegistrationService shardRegistrationService, CDNDownloadUrlService cdnDownloadUrlService) : base(logger) { _basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false) ? configuration.GetValue(nameof(StaticFilesServerConfiguration.ColdStorageDirectory)) @@ -48,6 +50,7 @@ public class ServerFilesController : ControllerBase _lightlessDbContext = lightlessDbContext; _metricsClient = metricsClient; _shardRegistrationService = shardRegistrationService; + _cdnDownloadUrlService = cdnDownloadUrlService; } [HttpPost(LightlessFiles.ServerFiles_DeleteAll)] @@ -105,6 +108,16 @@ public class ServerFilesController : ControllerBase baseUrl = shard.Value ?? _configuration.GetValue(nameof(StaticFilesServerConfiguration.CdnFullUrl)); } + var cdnDownloadUrl = string.Empty; + if (forbiddenFile == null) + { + var directUri = _cdnDownloadUrlService.TryCreateDirectDownloadUri(baseUrl, file.Hash); + if (directUri != null) + { + cdnDownloadUrl = directUri.ToString(); + } + } + response.Add(new DownloadFileDto { FileExists = file.Size > 0, @@ -113,6 +126,7 @@ public class ServerFilesController : ControllerBase Hash = file.Hash, Size = file.Size, Url = baseUrl?.ToString() ?? string.Empty, + CDNDownloadUrl = cdnDownloadUrl, RawSize = file.RawSize }); } @@ -127,6 +141,30 @@ public class ServerFilesController : ControllerBase return Ok(JsonSerializer.Serialize(allFileShards.SelectMany(t => t.RegionUris.Select(v => v.Value.ToString())))); } + [HttpGet(LightlessFiles.ServerFiles_DirectDownload + "/{hash}")] + [AllowAnonymous] + public async Task DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature) + { + if (!_cdnDownloadUrlService.DirectDownloadsEnabled) + { + return NotFound(); + } + + hash = hash.ToUpperInvariant(); + if (!_cdnDownloadUrlService.TryValidateSignature(hash, expires, signature)) + { + return Unauthorized(); + } + + var fileInfo = await _cachedFileProvider.DownloadAndGetLocalFileInfo(hash).ConfigureAwait(false); + if (fileInfo == null) + { + return NotFound(); + } + + return PhysicalFile(fileInfo.FullName, "application/octet-stream"); + } + [HttpPost(LightlessFiles.ServerFiles_FilesSend)] public async Task FilesSend([FromBody] FilesSendDto filesSendDto) { @@ -360,4 +398,4 @@ public class ServerFilesController : ControllerBase buffer[i] ^= 42; } } -} \ No newline at end of file +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadUrlService.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadUrlService.cs new file mode 100644 index 0000000..4bbc32b --- /dev/null +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Services/CDNDownloadUrlService.cs @@ -0,0 +1,108 @@ +using LightlessSync.API.Routes; +using LightlessSyncShared.Services; +using LightlessSyncShared.Utils.Configuration; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.WebUtilities; +using System.Security.Cryptography; +using System.Text; +using System.Globalization; +using System.Linq; + +namespace LightlessSyncStaticFilesServer.Services; + +public class CDNDownloadUrlService +{ + private readonly IConfigurationService _staticConfig; + private readonly IConfigurationService _globalConfig; + private readonly ILogger _logger; + + public CDNDownloadUrlService(IConfigurationService staticConfig, + IConfigurationService globalConfig, ILogger logger) + { + _staticConfig = staticConfig; + _globalConfig = globalConfig; + _logger = logger; + } + + public bool DirectDownloadsEnabled => + _staticConfig.GetValueOrDefault(nameof(StaticFilesServerConfiguration.EnableDirectDownloads), false); + + public Uri? TryCreateDirectDownloadUri(Uri? baseUri, string hash) + { + if (!DirectDownloadsEnabled || baseUri == null) + { + return null; + } + + if (!IsSupportedHash(hash)) + { + _logger.LogDebug("Skipping direct download link generation for invalid hash {hash}", hash); + return null; + } + + var normalizedHash = hash.ToUpperInvariant(); + + var lifetimeSeconds = Math.Max(5, + _staticConfig.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DirectDownloadTokenLifetimeSeconds), 300)); + var expiresAt = DateTimeOffset.UtcNow.AddSeconds(lifetimeSeconds); + var signature = CreateSignature(normalizedHash, expiresAt.ToUnixTimeSeconds()); + + var directPath = $"{LightlessFiles.ServerFiles}/{LightlessFiles.ServerFiles_DirectDownload}/{normalizedHash}"; + var builder = new UriBuilder(new Uri(baseUri, directPath)); + var query = new QueryBuilder + { + { "expires", expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture) }, + { "signature", signature } + }; + builder.Query = query.ToQueryString().Value!.TrimStart('?'); + return builder.Uri; + } + + public bool TryValidateSignature(string hash, long expiresUnixSeconds, string signature) + { + if (!DirectDownloadsEnabled) + { + return false; + } + + if (string.IsNullOrEmpty(signature) || !IsSupportedHash(hash)) + { + return false; + } + + var normalizedHash = hash.ToUpperInvariant(); + + DateTimeOffset expiresAt; + try + { + expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresUnixSeconds); + } + catch (ArgumentOutOfRangeException) + { + return false; + } + + if (expiresAt < DateTimeOffset.UtcNow) + { + return false; + } + + var expected = CreateSignature(normalizedHash, expiresAt.ToUnixTimeSeconds()); + return CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(expected), + Encoding.UTF8.GetBytes(signature)); + } + + private string CreateSignature(string hash, long expiresUnixSeconds) + { + var signingKey = _globalConfig.GetValue(nameof(LightlessConfigurationBase.Jwt)); + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey)); + var payload = Encoding.UTF8.GetBytes($"{hash}:{expiresUnixSeconds}"); + return WebEncoders.Base64UrlEncode(hmac.ComputeHash(payload)); + } + + private static bool IsSupportedHash(string hash) + { + return hash.Length == 40 && hash.All(char.IsAsciiLetterOrDigit); + } +} diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs b/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs index 1ed3ec3..f4034a5 100644 --- a/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/Startup.cs @@ -87,6 +87,7 @@ public class Startup services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(p => p.GetService()); services.AddHostedService(m => m.GetService()); services.AddSingleton, LightlessConfigurationServiceClient>(); diff --git a/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json b/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json index dc53c47..35f6258 100644 --- a/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json +++ b/LightlessSyncServer/LightlessSyncStaticFilesServer/appsettings.json @@ -25,7 +25,9 @@ "UnusedFileRetentionPeriodInDays": 7, "CacheDirectory": "G:\\ServerTest", "ServiceAddress": "http://localhost:5002", - "RemoteCacheSourceUri": "" + "RemoteCacheSourceUri": "", + "EnableDirectDownloads": true, + "DirectDownloadTokenLifetimeSeconds": 300 }, "AllowedHosts": "*" } From 0450255d6d91cc411ac65dcddfef4b1157ee776a Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 9 Oct 2025 18:00:06 -0500 Subject: [PATCH 38/40] Update Submodule Endpoint --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 89ac342..44fbe10 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 89ac34235d297655643684c425311cd68f0702f2 +Subproject commit 44fbe1045872fcae4df45e43625a9ff1a79bc2ef From a1f9526c23d6ccf13e0e776340e04b61176bddd5 Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 9 Oct 2025 18:03:30 -0500 Subject: [PATCH 39/40] quick quick quick cleanup --- .../LightlessSyncServer/Hubs/LightlessHub.User.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index ecd7025..a06b975 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -4,7 +4,6 @@ using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSyncServer.Utils; -using LightlessSyncServer.Configuration; using LightlessSyncShared.Metrics; using LightlessSyncShared.Models; using LightlessSyncShared.Utils; @@ -14,7 +13,6 @@ using Microsoft.EntityFrameworkCore; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using StackExchange.Redis; -using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; From aadfaca6294e1bd6861fa93d199c4587917c0e05 Mon Sep 17 00:00:00 2001 From: celine Date: Mon, 13 Oct 2025 00:07:54 +0200 Subject: [PATCH 40/40] .gitmodules updated Specified branch to omit errors on pulling submodules --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index 308c3b1..64bfa52 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "LightlessAPI"] path = LightlessAPI url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI + branch = main