From bbcf98576e2b1709b5e2f4cd0a0f154382f12375 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 17:22:25 +0200 Subject: [PATCH 01/11] Fixed so it can search on alias better --- .../LightlessSyncServer/Hubs/LightlessHub.Groups.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 7d4dc38..2e87207 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -746,7 +746,7 @@ public partial class LightlessHub var cancellationToken = RequestAbortedToken; var data = await DbContext.GroupProfiles - .FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID, cancellationToken) + .FirstOrDefaultAsync(g => g.Group.GID == dto.Group.AliasOrGID || g.Group.Alias == dto.Group.AliasOrGID, cancellationToken) .ConfigureAwait(false); var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); From 4fdc2a5c29b5eb4400462627521210112823e174 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 17:30:16 +0200 Subject: [PATCH 02/11] FIx to attempt to get group --- .../LightlessSyncServer/Hubs/LightlessHub.Groups.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 2e87207..3d2a448 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -746,7 +746,7 @@ public partial class LightlessHub var cancellationToken = RequestAbortedToken; var data = await DbContext.GroupProfiles - .FirstOrDefaultAsync(g => g.Group.GID == dto.Group.AliasOrGID || g.Group.Alias == dto.Group.AliasOrGID, cancellationToken) + .FirstOrDefaultAsync(g => g.Group.ToGroupData().AliasOrGID == dto.Group.AliasOrGID, cancellationToken) .ConfigureAwait(false); var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); From bab81aaf5145170254d69e1454459830d4582795 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 17:39:36 +0200 Subject: [PATCH 03/11] Added null checks --- .../LightlessSyncServer/Hubs/LightlessHub.Groups.cs | 9 +++++++-- .../LightlessSyncServer/Utils/Extensions.cs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 3d2a448..d489c8e 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -746,14 +746,19 @@ public partial class LightlessHub var cancellationToken = RequestAbortedToken; var data = await DbContext.GroupProfiles - .FirstOrDefaultAsync(g => g.Group.ToGroupData().AliasOrGID == dto.Group.AliasOrGID, cancellationToken) + .FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID, cancellationToken) .ConfigureAwait(false); var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); if (data is not null) { - profileDto = data.ToDTO(); + _logger.LogCallInfo(LightlessHubLogger.Args($"GroupGetProfile: dto={dto?.GID}, groupProfile.Group={(data?.Group != null)}")); + + if (data.Group != null) + { + profileDto = data.ToDTO(); + } } return profileDto; diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index 0c1ae44..2e4d62f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -11,7 +11,18 @@ public static class Extensions { public static GroupProfileDto ToDTO(this GroupProfile groupProfile) { - return new GroupProfileDto(groupProfile.Group.ToGroupData(), groupProfile.Description, groupProfile.Tags, groupProfile.Base64GroupProfileImage, groupProfile.IsNSFW, groupProfile.ProfileDisabled); + ArgumentNullException.ThrowIfNull(groupProfile); + + return groupProfile.Group == null + ? throw new InvalidOperationException("GroupProfile.Group is null when converting to DTO.") + : new GroupProfileDto( + groupProfile.Group.ToGroupData(), + groupProfile.Description, + groupProfile.Tags, + groupProfile.Base64GroupProfileImage, + groupProfile.IsNSFW, + groupProfile.ProfileDisabled + ); } public static GroupData ToGroupData(this Group group) From c30190704ffbbff0668aa3da9950680fe364f8da Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 17:53:20 +0200 Subject: [PATCH 04/11] Changed get/set profile with more safe handling --- .../Hubs/LightlessHub.Groups.cs | 103 +++++++++--------- .../LightlessSyncServer/Utils/Extensions.cs | 32 +++++- 2 files changed, 76 insertions(+), 59 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index d489c8e..adbe44d 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -745,23 +745,34 @@ public partial class LightlessHub var cancellationToken = RequestAbortedToken; - var data = await DbContext.GroupProfiles - .FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID, cancellationToken) - .ConfigureAwait(false); - - var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); - - if (data is not null) + if (dto?.Group == null) { - _logger.LogCallInfo(LightlessHubLogger.Args($"GroupGetProfile: dto={dto?.GID}, groupProfile.Group={(data?.Group != null)}")); - - if (data.Group != null) - { - profileDto = data.ToDTO(); - } + _logger.LogCallWarning(LightlessHubLogger.Args("GroupGetProfile: dto.Group is null")); + return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); } - return profileDto; + var data = await DbContext.GroupProfiles + .Include(gp => gp.Group) + .FirstOrDefaultAsync( + g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID, + cancellationToken + ) + .ConfigureAwait(false); + + if (data == null) + { + return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + } + + try + { + return data.ToDTO(); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args(ex, "GroupGetProfile: failed to map GroupProfileDto for {Group}", dto.Group.GID ?? dto.Group.AliasOrGID)); + return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + } } [Authorize(Policy = "Identified")] @@ -769,6 +780,8 @@ public partial class LightlessHub { _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + var cancellationToken = RequestAbortedToken; + if (dto.Group == null) return; var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); @@ -776,57 +789,39 @@ public partial class LightlessHub var groupProfileDb = await DbContext.GroupProfiles .FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID, - RequestAbortedToken) + cancellationToken) .ConfigureAwait(false); - if (groupProfileDb != null) + if (groupProfileDb == null) { - var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == groupProfileDb.GroupGID).Select(p => p.GroupUserUID).ToList(); - - if (string.Equals("", dto.PictureBase64, StringComparison.OrdinalIgnoreCase)) + groupProfileDb = new GroupProfile { - groupProfileDb.Base64GroupProfileImage = null; - } - else if (dto.PictureBase64 != null) - { - groupProfileDb.Base64GroupProfileImage = dto.PictureBase64; - } - - if (dto.Tags != null) - { - groupProfileDb.Tags = dto.Tags; - } - - if (dto.Description != null) - { - groupProfileDb.Description = dto.Description; - } - - if (dto.IsNsfw != null) - { - groupProfileDb.IsNSFW = dto.IsNsfw.Value; - } - - await Clients.Users(groupPairs).Client_GroupSendProfile(groupProfileDb.ToDTO()).ConfigureAwait(false); + GroupGID = dto.Group.GID, + ProfileDisabled = false, + IsNSFW = dto.IsNsfw ?? false, + }; + groupProfileDb.UpdateProfileFromDto(dto); + await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false); } else { - var groupProfile = new GroupProfile - { - GroupGID = dto.Group.GID, - Description = dto.Description, - Tags = dto.Tags, - Base64GroupProfileImage = dto.PictureBase64, - IsNSFW = false, - ProfileDisabled = false, - }; + groupProfileDb.UpdateProfileFromDto(dto); - await DbContext.GroupProfiles.AddAsync(groupProfile, - RequestAbortedToken) + var userIds = await DbContext.GroupPairs + .Where(p => p.GroupGID == groupProfileDb.GroupGID) + .Select(p => p.GroupUserUID) + .ToListAsync(cancellationToken) .ConfigureAwait(false); + + if (userIds.Count > 0) + { + var profileDto = groupProfileDb.ToDTO(); + await Clients.Users(userIds).Client_GroupSendProfile(profileDto) + .ConfigureAwait(false); + } } - await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); + await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } [Authorize(Policy = "Identified")] diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index 2e4d62f..f0cc026 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -9,14 +9,27 @@ namespace LightlessSyncServer.Utils; public static class Extensions { + public static void UpdateProfileFromDto(this GroupProfile profile, GroupProfileDto dto) + { + if (profile == null || dto == null) return; + + profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(dto.PictureBase64) ? null : dto.PictureBase64; + if (dto.Tags != null) profile.Tags = dto.Tags; + if (dto.Description != null) profile.Description = dto.Description; + if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value; + } + public static GroupProfileDto ToDTO(this GroupProfile groupProfile) { - ArgumentNullException.ThrowIfNull(groupProfile); + if (groupProfile == null) + { + return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + } - return groupProfile.Group == null - ? throw new InvalidOperationException("GroupProfile.Group is null when converting to DTO.") - : new GroupProfileDto( - groupProfile.Group.ToGroupData(), + var groupData = groupProfile.Group?.ToGroupData(); + + return new GroupProfileDto( + groupData, groupProfile.Description, groupProfile.Tags, groupProfile.Base64GroupProfileImage, @@ -27,16 +40,25 @@ public static class Extensions public static GroupData ToGroupData(this Group group) { + if (group == null) + return null; + return new GroupData(group.GID, group.Alias, group.CreatedDate); } public static UserData ToUserData(this GroupPair pair) { + if (pair == null) + return null; + return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias); } public static UserData ToUserData(this User user) { + if (user == null) + return null; + return new UserData(user.UID, user.Alias); } From ad00f7b07874341b72a80c7e32e1cd62c4dd0a72 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 18:36:08 +0200 Subject: [PATCH 05/11] Changes in database for tags to be array integers instead of strings --- ...dAndChangeTagsUserGroupProfile.Designer.cs | 1181 +++++++++++++++++ ...163402_AddAndChangeTagsUserGroupProfile.cs | 46 + .../LightlessDbContextModelSnapshot.cs | 8 +- .../Models/GroupProfile.cs | 2 +- .../Models/UserProfileData.cs | 1 + 5 files changed, 1235 insertions(+), 3 deletions(-) create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.Designer.cs create mode 100644 LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.Designer.cs new file mode 100644 index 0000000..c4665a9 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.Designer.cs @@ -0,0 +1,1181 @@ +// +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("20251019163402_AddAndChangeTagsUserGroupProfile")] + partial class AddAndChangeTagsUserGroupProfile + { + /// + 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("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.PrimitiveCollection("Tags") + .HasColumnType("integer[]") + .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") + .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)") + .HasColumnName("text_color_hex"); + + b.Property("TextGlowColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .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.PrimitiveCollection("Tags") + .HasColumnType("integer[]") + .HasColumnName("tags"); + + 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/20251019163402_AddAndChangeTagsUserGroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs new file mode 100644 index 0000000..cd87f85 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AddAndChangeTagsUserGroupProfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "tags", + table: "user_profile_data", + type: "integer[]", + nullable: true); + + migrationBuilder.AlterColumn( + name: "tags", + table: "group_profiles", + type: "integer[]", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "tags", + table: "user_profile_data"); + + migrationBuilder.AlterColumn( + name: "tags", + table: "group_profiles", + type: "text", + nullable: true, + oldClrType: typeof(int[]), + oldType: "integer[]", + oldNullable: true); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index eef5c03..ba9a000 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -605,8 +605,8 @@ namespace LightlessSyncServer.Migrations .HasColumnType("boolean") .HasColumnName("profile_disabled"); - b.Property("Tags") - .HasColumnType("text") + b.PrimitiveCollection("Tags") + .HasColumnType("integer[]") .HasColumnName("tags"); b.HasKey("GroupGID") @@ -840,6 +840,10 @@ namespace LightlessSyncServer.Migrations .HasColumnType("boolean") .HasColumnName("profile_disabled"); + b.PrimitiveCollection("Tags") + .HasColumnType("integer[]") + .HasColumnName("tags"); + b.Property("UserDescription") .HasColumnType("text") .HasColumnName("user_description"); diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs index 7cafdfe..b61b450 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs @@ -13,7 +13,7 @@ public class GroupProfile public string GroupGID { get; set; } public Group Group { get; set; } public string Description { get; set; } - public string Tags { get; set; } + public int[] Tags { get; set; } public string Base64GroupProfileImage { get; set; } public bool IsNSFW { get; set; } = false; public bool ProfileDisabled { get; set; } = false; diff --git a/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs b/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs index 0029b0e..ec7ba9b 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs @@ -12,6 +12,7 @@ public class UserProfileData public User User { get; set; } public string UserDescription { get; set; } + public int[] Tags { get; set; } [Required] [Key] From f35c0c4c2a8c7c0ea6a96ec604d71f0220f55fd9 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 18:40:49 +0200 Subject: [PATCH 06/11] updated submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 418e647..01688b2 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 418e647ef89a518dfd9faaaf831903f6c76de126 +Subproject commit 01688b27bcbe66a95736a682e643547040e9a1e4 From dba7536a7fbe385c6fffde7d86107ba1968ef1ae Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 20:58:07 +0200 Subject: [PATCH 07/11] Added tags on call for user profile calls, added disabled on syncshell profiles. reworked the calls --- .../Hubs/LightlessHub.Groups.cs | 47 ++++++++++ .../Hubs/LightlessHub.User.cs | 94 ++++++++++--------- .../LightlessSyncServer/Utils/Extensions.cs | 30 ++++++ 3 files changed, 125 insertions(+), 46 deletions(-) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index adbe44d..fe6ac8f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -9,6 +9,8 @@ using LightlessSyncShared.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using System.Security.Cryptography; namespace LightlessSyncServer.Hubs; @@ -764,6 +766,11 @@ public partial class LightlessHub return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); } + if (data.ProfileDisabled) + { + return new GroupProfileDto(Group: dto.Group, Description: "This profile was permanently disabled", Tags: [], PictureBase64: null, IsNsfw: false, IsDisabled: true); + } + try { return data.ToDTO(); @@ -792,6 +799,39 @@ public partial class LightlessHub cancellationToken) .ConfigureAwait(false); + + if (!string.IsNullOrEmpty(dto.PictureBase64)) + { + byte[] imageData; + try + { + imageData = Convert.FromBase64String(dto.PictureBase64); + } + catch (FormatException) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false); + return; + } + + MemoryStream ms = new(imageData); + await using (ms.ConfigureAwait(false)) + { + var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false); + if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); + return; + } + using var image = Image.Load(imageData); + + if (image.Width > 512 || image.Height > 512 || (imageData.Length > 2000 * 1024)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 512x512 or more than 2MiB").ConfigureAwait(false); + return; + } + } + } + if (groupProfileDb == null) { groupProfileDb = new GroupProfile @@ -800,11 +840,18 @@ public partial class LightlessHub ProfileDisabled = false, IsNSFW = dto.IsNsfw ?? false, }; + groupProfileDb.UpdateProfileFromDto(dto); await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false); } else { + if (groupProfileDb?.ProfileDisabled ?? false) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); + return; + } + groupProfileDb.UpdateProfileFromDto(dto); var userIds = await DbContext.GroupPairs diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index efa8c09..838118c 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -826,16 +826,16 @@ public partial class LightlessHub if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal)) { - return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); + 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: RequestAbortedToken).ConfigureAwait(false); - if (data == null) return new UserProfileDto(user.User, false, null, null, null); + 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"); - if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled"); + if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation", []); + if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled", []); - return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription); + return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription, data.Tags); } [Authorize(Policy = "Identified")] @@ -913,20 +913,20 @@ public partial class LightlessHub if (profile == null) { - return new UserProfileDto(userData, false, null, null, null); + return new UserProfileDto(userData, false, null, null, null, []); } if (profile.FlaggedForReport) { - return new UserProfileDto(userData, true, null, null, "This profile is flagged for report and pending evaluation"); + return new UserProfileDto(userData, true, null, null, "This profile is flagged for report and pending evaluation", []); } if (profile.ProfileDisabled) { - return new UserProfileDto(userData, true, null, null, "This profile was permanently disabled"); + return new UserProfileDto(userData, true, null, null, "This profile was permanently disabled", []); } - return new UserProfileDto(userData, false, profile.IsNSFW, profile.Base64ProfileImage, profile.UserDescription); + return new UserProfileDto(userData, false, profile.IsNSFW, profile.Base64ProfileImage, profile.UserDescription, profile.Tags); } [Authorize(Policy = "Identified")] @@ -1126,76 +1126,78 @@ public partial class LightlessHub { _logger.LogCallInfo(LightlessHubLogger.Args(dto)); + var cancellationToken = RequestAbortedToken; + 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: RequestAbortedToken).ConfigureAwait(false); - if (existingData?.FlaggedForReport ?? false) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false); - return; - } - - if (existingData?.ProfileDisabled ?? false) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); - return; - } - + //Image Check of size/format if (!string.IsNullOrEmpty(dto.ProfilePictureBase64)) { - byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64); - using MemoryStream ms = new(imageData); - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + byte[] imageData; + try { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); + imageData = Convert.FromBase64String(dto.ProfilePictureBase64); + } + catch (FormatException) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false); return; } - using var image = Image.Load(imageData); - if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024)) + MemoryStream ms = new(imageData); + await using (ms.ConfigureAwait(false)) { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false); - return; + var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false); + if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); + return; + } + using var image = Image.Load(imageData); + + if (image.Width > 512 || image.Height > 512 || (imageData.Length > 2000 * 1024)) + { + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 512x512 or more than 2MiB.").ConfigureAwait(false); + return; + } } } if (existingData != null) { - if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase)) + if (existingData.FlaggedForReport) { - existingData.Base64ProfileImage = null; - } - else if (dto.ProfilePictureBase64 != null) - { - existingData.Base64ProfileImage = dto.ProfilePictureBase64; + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false); + return; } - if (dto.IsNSFW != null) + if (existingData.ProfileDisabled) { - existingData.IsNSFW = dto.IsNSFW.Value; + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); + return; } - if (dto.Description != null) - { - existingData.UserDescription = dto.Description; - } + existingData.UpdateProfileFromDto(dto); } else { - UserProfileData userProfileData = new() + UserProfileData newUserProfileData = new() { UserUID = dto.User.UID, Base64ProfileImage = dto.ProfilePictureBase64 ?? null, UserDescription = dto.Description ?? null, - IsNSFW = dto.IsNSFW ?? false + IsNSFW = dto.IsNSFW ?? false, }; - await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false); + newUserProfileData.UpdateProfileFromDto(dto); + + await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false); } - await DbContext.SaveChangesAsync().ConfigureAwait(false); + + await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false); diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index f0cc026..1a1130f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -2,6 +2,7 @@ using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; using LightlessSyncShared.Models; using static LightlessSyncServer.Hubs.LightlessHub; @@ -19,6 +20,16 @@ public static class Extensions if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value; } + public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto) + { + if (profile == null || dto == null) return; + + profile.Base64ProfileImage = string.IsNullOrWhiteSpace(dto.ProfilePictureBase64) ? null : dto.ProfilePictureBase64; + if (dto.Tags != null) profile.Tags = dto.Tags; + if (dto.Description != null) profile.UserDescription = dto.Description; + if (dto.IsNSFW.HasValue) profile.IsNSFW = dto.IsNSFW.Value; + } + public static GroupProfileDto ToDTO(this GroupProfile groupProfile) { if (groupProfile == null) @@ -38,6 +49,25 @@ public static class Extensions ); } + public static UserProfileDto ToDTO(this UserProfileData userProfileData) + { + if (userProfileData == null) + { + return new UserProfileDto(User: null, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null, Tags: []); + } + + var userData = userProfileData.User?.ToUserData(); + + return new UserProfileDto( + userData, + userProfileData.ProfileDisabled, + userProfileData.IsNSFW, + userProfileData.Base64ProfileImage, + userProfileData.UserDescription, + userProfileData.Tags + ); + } + public static GroupData ToGroupData(this Group group) { if (group == null) From 7cc6918b1246e66e75b1521c1b06a73fd515fc2d Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 20:58:40 +0200 Subject: [PATCH 08/11] updated submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 01688b2..9ac9168 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 01688b27bcbe66a95736a682e643547040e9a1e4 +Subproject commit 9ac91682e630c76d61878a03a364ff558cf04146 From d28198a9c88990d2da1f3b9c6d26fd4c8365b8dd Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 21:04:12 +0200 Subject: [PATCH 09/11] fix migrations --- .../20251019163402_AddAndChangeTagsUserGroupProfile.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs index cd87f85..d6a110e 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs @@ -16,6 +16,8 @@ namespace LightlessSyncServer.Migrations type: "integer[]", nullable: true); + migrationBuilder.Sql("UPDATE group_profiles SET tags = NULL;"); + migrationBuilder.AlterColumn( name: "tags", table: "group_profiles", @@ -29,10 +31,12 @@ namespace LightlessSyncServer.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropColumn( name: "tags", table: "user_profile_data"); + migrationBuilder.AlterColumn( name: "tags", table: "group_profiles", From 3926f3be894a8da536b0fb843aeada9e9aee06e5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 21:04:22 +0200 Subject: [PATCH 10/11] remove blankspace --- .../20251019163402_AddAndChangeTagsUserGroupProfile.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs index d6a110e..9dcba24 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20251019163402_AddAndChangeTagsUserGroupProfile.cs @@ -36,7 +36,6 @@ namespace LightlessSyncServer.Migrations name: "tags", table: "user_profile_data"); - migrationBuilder.AlterColumn( name: "tags", table: "group_profiles", From 884ad25c3304bced60b2882c7db3619ad630aff5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Sun, 19 Oct 2025 21:21:19 +0200 Subject: [PATCH 11/11] update submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 9ac9168..0bc7abb 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 9ac91682e630c76d61878a03a364ff558cf04146 +Subproject commit 0bc7abb274548bcde36c65ef1cf9f1a143d6492c