diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs index 3290ee9b..0fe01110 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs @@ -1,18 +1,41 @@ using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers; -public class NoteRenderer(IOptions config, UserRenderer userRenderer) { - public async Task RenderAsync(Note note, int recurse = 2) { +public class NoteRenderer( + IOptions config, + UserRenderer userRenderer, + MfmConverter mfmConverter, + DatabaseContext db +) { + public async Task RenderAsync(Note note, List? mentions = null, int recurse = 2) { var uri = note.Uri ?? $"https://{config.Value.WebDomain}/notes/{note.Id}"; - var renote = note.Renote != null && recurse > 0 ? await RenderAsync(note.Renote, --recurse) : null; + var renote = note.Renote != null && recurse > 0 ? await RenderAsync(note.Renote, mentions, --recurse) : null; var text = note.Text; //TODO: append quote uri - var content = text != null ? await MfmConverter.ToHtmlAsync(text) : null; + var content = text != null ? await mfmConverter.ToHtmlAsync(text, note.MentionedRemoteUsers) : null; + + if (mentions == null) { + mentions = await db.Users.Where(p => note.Mentions.Contains(p.Id)) + .Select(u => new Mention { + Id = u.Id, + Username = u.Username, + Acct = u.Acct, + Url = (u.UserProfile != null + ? u.UserProfile.Url ?? u.Uri + : u.Uri) ?? $"https://{config.Value.WebDomain}/@{u.Username}" + }) + .ToListAsync(); + } + else { + mentions = [..mentions.Where(p => note.Mentions.Contains(p.Id))]; + } var res = new Status { Id = note.Id, @@ -38,13 +61,30 @@ public class NoteRenderer(IOptions config, UserRenderer Visibility = Status.EncodeVisibility(note.Visibility), Content = content, Text = text, + Mentions = mentions, IsPinned = false //FIXME }; return res; } + private async Task> GetMentions(IReadOnlyCollection notes) { + var ids = notes.SelectMany(n => n.Mentions).Distinct(); + return await db.Users.Where(p => ids.Contains(p.Id)) + .Select(u => new Mention { + Id = u.Id, + Username = u.Username, + Acct = u.Acct, + Url = u.UserProfile != null + ? u.UserProfile.Url ?? u.Uri ?? $"https://{config.Value.WebDomain}/@{u.Username}" + : u.Uri ?? $"https://{config.Value.WebDomain}/@{u.Username}" + }) + .ToListAsync(); + } + public async Task> RenderManyAsync(IEnumerable notes) { - return await notes.Select(RenderAsync).AwaitAllAsync(); + var noteList = notes.ToList(); + var mentions = await GetMentions(noteList); + return await noteList.Select(async p => await RenderAsync(p, mentions)).AwaitAllAsync(); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs index acaa1463..6d88e8c0 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers; -public class UserRenderer(IOptions config) { +public class UserRenderer(IOptions config, MfmConverter mfmConverter) { private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png"; public async Task RenderAsync(User user, UserProfile? profile) { @@ -29,7 +29,7 @@ public class UserRenderer(IOptions config) { FollowersCount = user.FollowersCount, FollowingCount = user.FollowingCount, StatusesCount = user.NotesCount, - Note = await MfmConverter.ToHtmlAsync(profile?.Description ?? ""), + Note = await mfmConverter.ToHtmlAsync(profile?.Description ?? "", []), Url = profile?.Url ?? user.Uri ?? $"https://{user.Host ?? config.Value.WebDomain}/@{user.Username}", AvatarStaticUrl = user.AvatarUrl ?? _transparent, //TODO HeaderUrl = user.BannerUrl ?? _transparent, diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Mention.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Mention.cs new file mode 100644 index 00000000..8cfaa6a1 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Mention.cs @@ -0,0 +1,10 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; + +public class Mention { + [J("id")] public required string Id { get; set; } + [J("username")] public required string Username { get; set; } + [J("acct")] public required string Acct { get; set; } + [J("url")] public required string Url { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Status.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Status.cs index 2ad8a7dd..d9a04462 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Status.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/Status.cs @@ -40,9 +40,10 @@ public class Status : IEntity { [JI(Condition = JsonIgnoreCondition.WhenWritingNull)] public required bool? IsPinned { get; set; } + [J("mentions")] public required List Mentions { get; set; } + [J("emojis")] public object[] Emojis => []; //FIXME [J("media_attachments")] public object[] Media => []; //FIXME - [J("mentions")] public object[] Mentions => []; //FIXME [J("tags")] public object[] Tags => []; //FIXME [J("reactions")] public object[] Reactions => []; //FIXME [J("filtered")] public object[] Filtered => []; //FIXME diff --git a/Iceshrimp.Backend/Core/Database/DatabaseContext.cs b/Iceshrimp.Backend/Core/Database/DatabaseContext.cs index ba507cde..4e3e0742 100644 --- a/Iceshrimp.Backend/Core/Database/DatabaseContext.cs +++ b/Iceshrimp.Backend/Core/Database/DatabaseContext.cs @@ -531,7 +531,7 @@ public class DatabaseContext(DbContextOptions options) entity.Property(e => e.FileIds).HasDefaultValueSql("'{}'::character varying[]"); entity.Property(e => e.HasPoll).HasDefaultValue(false); entity.Property(e => e.LocalOnly).HasDefaultValue(false); - entity.Property(e => e.MentionedRemoteUsers).HasDefaultValueSql("'[]'::text"); + entity.Property(e => e.MentionedRemoteUsers).HasDefaultValueSql("'[]'::jsonb"); entity.Property(e => e.Mentions).HasDefaultValueSql("'{}'::character varying[]"); entity.Property(e => e.Reactions).HasDefaultValueSql("'{}'::jsonb"); entity.Property(e => e.RenoteCount).HasDefaultValue((short)0); diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240205190843_RefactorJsonbColumns.Designer.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240205190843_RefactorJsonbColumns.Designer.cs index 529c8276..bd708679 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/20240205190843_RefactorJsonbColumns.Designer.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240205190843_RefactorJsonbColumns.Designer.cs @@ -4418,7 +4418,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("location") .HasComment("The location of the User."); - b.Property>("Mentions") + b.Property>("Mentions") .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("jsonb") diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240207164121_AddFederationControl.Designer.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240207164121_AddFederationControl.Designer.cs index 00d87379..661db1e0 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/20240207164121_AddFederationControl.Designer.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240207164121_AddFederationControl.Designer.cs @@ -4455,7 +4455,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("location") .HasComment("The location of the User."); - b.Property>("Mentions") + b.Property>("Mentions") .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("jsonb") diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240209170817_MoveDriveHashAlgoToSha256.Designer.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240209170817_MoveDriveHashAlgoToSha256.Designer.cs index 4ced1a3d..a334aa65 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/20240209170817_MoveDriveHashAlgoToSha256.Designer.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240209170817_MoveDriveHashAlgoToSha256.Designer.cs @@ -4454,7 +4454,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("location") .HasComment("The location of the User."); - b.Property>("Mentions") + b.Property>("Mentions") .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("jsonb") diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240210151527_UserAlsoKnownAsToArray.Designer.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240210151527_UserAlsoKnownAsToArray.Designer.cs index bd07f239..054b8d59 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/20240210151527_UserAlsoKnownAsToArray.Designer.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240210151527_UserAlsoKnownAsToArray.Designer.cs @@ -4454,7 +4454,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("location") .HasComment("The location of the User."); - b.Property>("Mentions") + b.Property>("Mentions") .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("jsonb") diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240210233409_FixNoteMentionsColumnType.Designer.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240210233409_FixNoteMentionsColumnType.Designer.cs new file mode 100644 index 00000000..ea4c5ccb --- /dev/null +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240210233409_FixNoteMentionsColumnType.Designer.cs @@ -0,0 +1,5963 @@ +// +using System; +using System.Collections.Generic; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Iceshrimp.Backend.Core.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240210233409_FixNoteMentionsColumnType")] + partial class FixNoteMentionsColumnType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "antenna_src_enum", new[] { "home", "all", "users", "list", "group", "instances" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "note_visibility_enum", new[] { "public", "home", "followers", "specified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type_enum", new[] { "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "page_visibility_enum", new[] { "public", "followers", "specified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "relay_status_enum", new[] { "requesting", "accepted", "rejected" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_profile_ffvisibility_enum", new[] { "public", "followers", "private" }); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pg_trgm"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AbuseUserReport", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AssigneeId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("assigneeId"); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("comment"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the AbuseUserReport."); + + b.Property("Forwarded") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("forwarded"); + + b.Property("ReporterHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("reporterHost") + .HasComment("[Denormalized]"); + + b.Property("ReporterId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("reporterId"); + + b.Property("Resolved") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("resolved"); + + b.Property("TargetUserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("targetUserHost") + .HasComment("[Denormalized]"); + + b.Property("TargetUserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("targetUserId"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ReporterHost"); + + b.HasIndex("ReporterId"); + + b.HasIndex("Resolved"); + + b.HasIndex("TargetUserHost"); + + b.HasIndex("TargetUserId"); + + b.ToTable("abuse_user_report"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AccessToken", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AppId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("appId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the AccessToken."); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("description"); + + b.Property("Fetched") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("fetched"); + + b.Property("Hash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("hash"); + + b.Property("IconUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("iconUrl"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastUsedAt"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name"); + + b.Property>("Permission") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(64)[]") + .HasColumnName("permission") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Session") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("session"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("AppId"); + + b.HasIndex("Hash"); + + b.HasIndex("Session"); + + b.HasIndex("Token"); + + b.HasIndex("UserId"); + + b.ToTable("access_token"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AllowedInstance", b => + { + b.Property("Host") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("host"); + + b.Property("IsImported") + .HasColumnType("boolean") + .HasColumnName("imported"); + + b.HasKey("Host"); + + b.ToTable("allowed_instance"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Announcement", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Announcement."); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("imageUrl"); + + b.Property("IsGoodNews") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isGoodNews"); + + b.Property("ShowPopup") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("showPopup"); + + b.Property("Text") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the Announcement."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("announcement"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AnnouncementRead", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AnnouncementId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("announcementId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the AnnouncementRead."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "AnnouncementId") + .IsUnique(); + + b.ToTable("announcement_read"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Antenna", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CaseSensitive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("caseSensitive"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Antenna."); + + b.Property>>("ExcludeKeywords") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("excludeKeywords") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Expression") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("expression"); + + b.Property>("Instances") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("instances") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property>>("Keywords") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("keywords") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the Antenna."); + + b.Property("Notify") + .HasColumnType("boolean") + .HasColumnName("notify"); + + b.Property("Source") + .HasColumnType("antenna_src_enum") + .HasColumnName("src"); + + b.Property("UserGroupMemberId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userGroupMemberId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.Property("UserListId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userListId"); + + b.Property>("Users") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(1024)[]") + .HasColumnName("users") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("WithFile") + .HasColumnType("boolean") + .HasColumnName("withFile"); + + b.Property("WithReplies") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("withReplies"); + + b.HasKey("Id"); + + b.HasIndex("UserGroupMemberId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserListId"); + + b.ToTable("antenna"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.App", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CallbackUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("callbackUrl") + .HasComment("The callbackUrl of the App."); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the App."); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("description") + .HasComment("The description of the App."); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the App."); + + b.Property>("Permission") + .IsRequired() + .HasColumnType("character varying(64)[]") + .HasColumnName("permission") + .HasComment("The permission of the App."); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("secret") + .HasComment("The secret key of the App."); + + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Secret"); + + b.HasIndex("UserId"); + + b.ToTable("app"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AttestationChallenge", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.Property("Challenge") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("challenge") + .HasComment("Hex-encoded sha256 hash of the challenge."); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The date challenge was created for expiry purposes."); + + b.Property("RegistrationChallenge") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("registrationChallenge") + .HasComment("Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication."); + + b.HasKey("Id", "UserId"); + + b.HasIndex("Challenge"); + + b.HasIndex("UserId"); + + b.ToTable("attestation_challenge"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AuthSession", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("appId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the AuthSession."); + + b.Property("Token") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token"); + + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("AppId"); + + b.HasIndex("Token"); + + b.HasIndex("UserId"); + + b.ToTable("auth_session"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.BlockedInstance", b => + { + b.Property("Host") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("host"); + + b.Property("IsImported") + .HasColumnType("boolean") + .HasColumnName("imported"); + + b.Property("Reason") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.HasKey("Host"); + + b.ToTable("blocked_instance"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Blocking", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("BlockeeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("blockeeId") + .HasComment("The blockee user ID."); + + b.Property("BlockerId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("blockerId") + .HasComment("The blocker user ID."); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Blocking."); + + b.HasKey("Id"); + + b.HasIndex("BlockeeId"); + + b.HasIndex("BlockerId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("BlockerId", "BlockeeId") + .IsUnique(); + + b.ToTable("blocking"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Channel", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("BannerId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("bannerId") + .HasComment("The ID of banner Channel."); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Channel."); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("description") + .HasComment("The description of the Channel."); + + b.Property("LastNotedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastNotedAt"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the Channel."); + + b.Property("NotesCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("notesCount") + .HasComment("The count of notes."); + + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.Property("UsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("usersCount") + .HasComment("The count of users."); + + b.HasKey("Id"); + + b.HasIndex("BannerId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("LastNotedAt"); + + b.HasIndex("NotesCount"); + + b.HasIndex("UserId"); + + b.HasIndex("UsersCount"); + + b.ToTable("channel"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ChannelFollowing", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the ChannelFollowing."); + + b.Property("FolloweeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followeeId") + .HasComment("The followee channel ID."); + + b.Property("FollowerId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followerId") + .HasComment("The follower user ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FolloweeId"); + + b.HasIndex("FollowerId"); + + b.HasIndex("FollowerId", "FolloweeId") + .IsUnique(); + + b.ToTable("channel_following"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ChannelNotePin", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("ChannelId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channelId"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the ChannelNotePin."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("NoteId"); + + b.HasIndex("ChannelId", "NoteId") + .IsUnique(); + + b.ToTable("channel_note_pin"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Clip", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Clip."); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("description") + .HasComment("The description of the Clip."); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isPublic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the Clip."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("clip"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ClipNote", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("ClipId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("clipId") + .HasComment("The clip ID."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId") + .HasComment("The note ID."); + + b.HasKey("Id"); + + b.HasIndex("ClipId"); + + b.HasIndex("NoteId"); + + b.HasIndex("NoteId", "ClipId") + .IsUnique(); + + b.ToTable("clip_note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.DriveFile", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AccessKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("accessKey"); + + b.Property("Blurhash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("blurhash") + .HasComment("The BlurHash string."); + + b.Property("Comment") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("comment") + .HasComment("The comment of the DriveFile."); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the DriveFile."); + + b.Property("FolderId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("folderId") + .HasComment("The parent folder ID. If null, it means the DriveFile is located in root."); + + b.Property("IsLink") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isLink") + .HasComment("Whether the DriveFile is direct link to remote server."); + + b.Property("IsSensitive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isSensitive") + .HasComment("Whether the DriveFile is NSFW."); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name") + .HasComment("The file name of the DriveFile."); + + b.Property("Properties") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("properties") + .HasDefaultValueSql("'{}'::jsonb") + .HasComment("The any properties of the DriveFile. For example, it includes image width/height."); + + b.Property>("RequestHeaders") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("requestHeaders") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RequestIp") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("requestIp"); + + b.Property("Sha256") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("sha256") + .HasComment("The SHA256 hash of the DriveFile."); + + b.Property("Size") + .HasColumnType("integer") + .HasColumnName("size") + .HasComment("The file size (bytes) of the DriveFile."); + + b.Property("Src") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("src"); + + b.Property("StoredInternal") + .HasColumnType("boolean") + .HasColumnName("storedInternal"); + + b.Property("ThumbnailAccessKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("thumbnailAccessKey"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("thumbnailUrl") + .HasComment("The URL of the thumbnail of the DriveFile."); + + b.Property("Type") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("type") + .HasComment("The content type (MIME) of the DriveFile."); + + b.Property("Uri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("uri") + .HasComment("The URI of the DriveFile. it will be null when the DriveFile is local."); + + b.Property("Url") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("url") + .HasComment("The URL of the DriveFile."); + + b.Property("UserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("userHost") + .HasComment("The host of owner. It will be null if the user in local."); + + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.Property("WebpublicAccessKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("webpublicAccessKey"); + + b.Property("WebpublicType") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("webpublicType"); + + b.Property("WebpublicUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("webpublicUrl") + .HasComment("The URL of the webpublic of the DriveFile."); + + b.HasKey("Id"); + + b.HasIndex("AccessKey") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FolderId"); + + b.HasIndex("IsLink"); + + b.HasIndex("IsSensitive"); + + b.HasIndex("Sha256"); + + b.HasIndex("ThumbnailAccessKey") + .IsUnique(); + + b.HasIndex("Type"); + + b.HasIndex("Uri"); + + b.HasIndex("UserHost"); + + b.HasIndex("UserId"); + + b.HasIndex("WebpublicAccessKey") + .IsUnique(); + + b.HasIndex("UserId", "FolderId", "Id"); + + b.ToTable("drive_file"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.DriveFolder", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the DriveFolder."); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the DriveFolder."); + + b.Property("ParentId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("parentId") + .HasComment("The parent folder ID. If null, it means the DriveFolder is located in root."); + + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ParentId"); + + b.HasIndex("UserId"); + + b.ToTable("drive_folder"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Emoji", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property>("Aliases") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("aliases") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Category") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("category"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height") + .HasComment("Image height"); + + b.Property("Host") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("host"); + + b.Property("License") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("license"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name"); + + b.Property("OriginalUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("originalUrl"); + + b.Property("PublicUrl") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("publicUrl") + .HasDefaultValueSql("''::character varying"); + + b.Property("Type") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.Property("Uri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("uri"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width") + .HasComment("Image width"); + + b.HasKey("Id"); + + b.HasIndex("Host"); + + b.HasIndex("Name"); + + b.HasIndex("Name", "Host") + .IsUnique(); + + b.ToTable("emoji"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.FollowRequest", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the FollowRequest."); + + b.Property("FolloweeHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followeeHost") + .HasComment("[Denormalized]"); + + b.Property("FolloweeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followeeId") + .HasComment("The followee user ID."); + + b.Property("FolloweeInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followeeInbox") + .HasComment("[Denormalized]"); + + b.Property("FolloweeSharedInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followeeSharedInbox") + .HasComment("[Denormalized]"); + + b.Property("FollowerHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followerHost") + .HasComment("[Denormalized]"); + + b.Property("FollowerId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followerId") + .HasComment("The follower user ID."); + + b.Property("FollowerInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followerInbox") + .HasComment("[Denormalized]"); + + b.Property("FollowerSharedInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followerSharedInbox") + .HasComment("[Denormalized]"); + + b.Property("RequestId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("requestId") + .HasComment("id of Follow Activity."); + + b.HasKey("Id"); + + b.HasIndex("FolloweeId"); + + b.HasIndex("FollowerId"); + + b.HasIndex("FollowerId", "FolloweeId") + .IsUnique(); + + b.ToTable("follow_request"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Following", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Following."); + + b.Property("FolloweeHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followeeHost") + .HasComment("[Denormalized]"); + + b.Property("FolloweeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followeeId") + .HasComment("The followee user ID."); + + b.Property("FolloweeInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followeeInbox") + .HasComment("[Denormalized]"); + + b.Property("FolloweeSharedInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followeeSharedInbox") + .HasComment("[Denormalized]"); + + b.Property("FollowerHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followerHost") + .HasComment("[Denormalized]"); + + b.Property("FollowerId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followerId") + .HasComment("The follower user ID."); + + b.Property("FollowerInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followerInbox") + .HasComment("[Denormalized]"); + + b.Property("FollowerSharedInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followerSharedInbox") + .HasComment("[Denormalized]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FolloweeHost"); + + b.HasIndex("FolloweeId"); + + b.HasIndex("FollowerHost"); + + b.HasIndex("FollowerId"); + + b.HasIndex("FollowerId", "FolloweeId") + .IsUnique(); + + b.ToTable("following"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.GalleryLike", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("PostId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("postId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "PostId") + .IsUnique(); + + b.ToTable("gallery_like"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.GalleryPost", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the GalleryPost."); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("description"); + + b.Property>("FileIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("fileIds") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("IsSensitive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isSensitive") + .HasComment("Whether the post is sensitive."); + + b.Property("LikedCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("likedCount"); + + b.Property>("Tags") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("tags") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the GalleryPost."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The ID of author."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FileIds"); + + b.HasIndex("IsSensitive"); + + b.HasIndex("LikedCount"); + + b.HasIndex("Tags"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("UserId"); + + b.ToTable("gallery_post"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Hashtag", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property>("AttachedLocalUserIds") + .IsRequired() + .HasColumnType("character varying(32)[]") + .HasColumnName("attachedLocalUserIds"); + + b.Property("AttachedLocalUsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("attachedLocalUsersCount"); + + b.Property>("AttachedRemoteUserIds") + .IsRequired() + .HasColumnType("character varying(32)[]") + .HasColumnName("attachedRemoteUserIds"); + + b.Property("AttachedRemoteUsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("attachedRemoteUsersCount"); + + b.Property>("AttachedUserIds") + .IsRequired() + .HasColumnType("character varying(32)[]") + .HasColumnName("attachedUserIds"); + + b.Property("AttachedUsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("attachedUsersCount"); + + b.Property>("MentionedLocalUserIds") + .IsRequired() + .HasColumnType("character varying(32)[]") + .HasColumnName("mentionedLocalUserIds"); + + b.Property("MentionedLocalUsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("mentionedLocalUsersCount"); + + b.Property>("MentionedRemoteUserIds") + .IsRequired() + .HasColumnType("character varying(32)[]") + .HasColumnName("mentionedRemoteUserIds"); + + b.Property("MentionedRemoteUsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("mentionedRemoteUsersCount"); + + b.Property>("MentionedUserIds") + .IsRequired() + .HasColumnType("character varying(32)[]") + .HasColumnName("mentionedUserIds"); + + b.Property("MentionedUsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("mentionedUsersCount"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.HasIndex("AttachedLocalUsersCount"); + + b.HasIndex("AttachedRemoteUsersCount"); + + b.HasIndex("AttachedUsersCount"); + + b.HasIndex("MentionedLocalUsersCount"); + + b.HasIndex("MentionedRemoteUsersCount"); + + b.HasIndex("MentionedUsersCount"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("hashtag"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.HtmlNoteCacheEntry", b => + { + b.Property("NoteId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("NoteId"); + + b.ToTable("html_note_cache_entry"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.HtmlUserCacheEntry", b => + { + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("Fields") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("fields") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt"); + + b.HasKey("UserId"); + + b.ToTable("html_user_cache_entry"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Instance", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CaughtAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("caughtAt") + .HasComment("The caught date of the Instance."); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("FaviconUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("faviconUrl"); + + b.Property("FollowersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("followersCount"); + + b.Property("FollowingCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("followingCount"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("host") + .HasComment("The host of the Instance."); + + b.Property("IconUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("iconUrl"); + + b.Property("InfoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("infoUpdatedAt"); + + b.Property("IsNotResponding") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isNotResponding"); + + b.Property("IsSuspended") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isSuspended"); + + b.Property("LastCommunicatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastCommunicatedAt"); + + b.Property("LatestRequestReceivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("latestRequestReceivedAt"); + + b.Property("LatestRequestSentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("latestRequestSentAt"); + + b.Property("LatestStatus") + .HasColumnType("integer") + .HasColumnName("latestStatus"); + + b.Property("MaintainerEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("maintainerEmail"); + + b.Property("MaintainerName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("maintainerName"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NotesCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("notesCount") + .HasComment("The count of the notes of the Instance."); + + b.Property("OpenRegistrations") + .HasColumnType("boolean") + .HasColumnName("openRegistrations"); + + b.Property("SoftwareName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("softwareName") + .HasComment("The software of the Instance."); + + b.Property("SoftwareVersion") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("softwareVersion"); + + b.Property("ThemeColor") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("themeColor"); + + b.Property("UsersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("usersCount") + .HasComment("The count of the users of the Instance."); + + b.HasKey("Id"); + + b.HasIndex("CaughtAt"); + + b.HasIndex("Host") + .IsUnique(); + + b.HasIndex("IsSuspended"); + + b.ToTable("instance"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.MessagingMessage", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the MessagingMessage."); + + b.Property("FileId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("fileId"); + + b.Property("GroupId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("groupId") + .HasComment("The recipient group ID."); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isRead"); + + b.Property>("Reads") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("reads") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("RecipientId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("recipientId") + .HasComment("The recipient user ID."); + + b.Property("Text") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("text"); + + b.Property("Uri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("uri"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The sender user ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FileId"); + + b.HasIndex("GroupId"); + + b.HasIndex("RecipientId"); + + b.HasIndex("UserId"); + + b.ToTable("messaging_message"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Meta", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property>("AllowedHosts") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("allowedHosts") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("AutofollowedAccount") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("autofollowedAccount"); + + b.Property("BackgroundImageUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("backgroundImageUrl"); + + b.Property("BannerUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("bannerUrl"); + + b.Property>("BlockedHosts") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("blockedHosts") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("CacheRemoteFiles") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("cacheRemoteFiles"); + + b.Property>("CustomMotd") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("customMOTD") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property>("CustomSplashIcons") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("customSplashIcons") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("DeeplAuthKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("deeplAuthKey"); + + b.Property("DeeplIsPro") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("deeplIsPro"); + + b.Property("DefaultDarkTheme") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("defaultDarkTheme"); + + b.Property("DefaultLightTheme") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("defaultLightTheme"); + + b.Property("DefaultReaction") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("defaultReaction") + .HasDefaultValueSql("'⭐'::character varying"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("description"); + + b.Property("DisableGlobalTimeline") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("disableGlobalTimeline"); + + b.Property("DisableLocalTimeline") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("disableLocalTimeline"); + + b.Property("DisableRecommendedTimeline") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("disableRecommendedTimeline"); + + b.Property("DisableRegistration") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("disableRegistration"); + + b.Property("DiscordClientId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("discordClientId"); + + b.Property("DiscordClientSecret") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("discordClientSecret"); + + b.Property("DonationLink") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("donationLink"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("email"); + + b.Property("EmailRequiredForSignup") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("emailRequiredForSignup"); + + b.Property("EnableActiveEmailValidation") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("enableActiveEmailValidation"); + + b.Property("EnableDiscordIntegration") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableDiscordIntegration"); + + b.Property("EnableEmail") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableEmail"); + + b.Property("EnableGithubIntegration") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableGithubIntegration"); + + b.Property("EnableHcaptcha") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableHcaptcha"); + + b.Property("EnableIdenticonGeneration") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("enableIdenticonGeneration"); + + b.Property("EnableIpLogging") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableIpLogging"); + + b.Property("EnableRecaptcha") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableRecaptcha"); + + b.Property("EnableServerMachineStats") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableServerMachineStats"); + + b.Property("ErrorImageUrl") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("errorImageUrl") + .HasDefaultValueSql("'/static-assets/badges/error.png'::character varying"); + + b.Property>("ExperimentalFeatures") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("experimentalFeatures") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("FeedbackUrl") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("feedbackUrl") + .HasDefaultValueSql("'https://iceshrimp.dev/iceshrimp/iceshrimp/issues/new'::character varying"); + + b.Property("GithubClientId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("githubClientId"); + + b.Property("GithubClientSecret") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("githubClientSecret"); + + b.Property("HcaptchaSecretKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hcaptchaSecretKey"); + + b.Property("HcaptchaSiteKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hcaptchaSiteKey"); + + b.Property>("HiddenTags") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("hiddenTags") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("IconUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("iconUrl"); + + b.Property>("Langs") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(64)[]") + .HasColumnName("langs") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("LibreTranslateApiKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("libreTranslateApiKey"); + + b.Property("LibreTranslateApiUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("libreTranslateApiUrl"); + + b.Property("LocalDriveCapacityMb") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1024) + .HasColumnName("localDriveCapacityMb") + .HasComment("Drive capacity of a local user (MB)"); + + b.Property("LogoImageUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("logoImageUrl"); + + b.Property("MaintainerEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("maintainerEmail"); + + b.Property("MaintainerName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("maintainerName"); + + b.Property("MascotImageUrl") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("mascotImageUrl") + .HasDefaultValueSql("'/static-assets/badges/info.png'::character varying"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name"); + + b.Property("ObjectStorageAccessKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStorageAccessKey"); + + b.Property("ObjectStorageBaseUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStorageBaseUrl"); + + b.Property("ObjectStorageBucket") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStorageBucket"); + + b.Property("ObjectStorageEndpoint") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStorageEndpoint"); + + b.Property("ObjectStoragePort") + .HasColumnType("integer") + .HasColumnName("objectStoragePort"); + + b.Property("ObjectStoragePrefix") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStoragePrefix"); + + b.Property("ObjectStorageRegion") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStorageRegion"); + + b.Property("ObjectStorageS3ForcePathStyle") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("objectStorageS3ForcePathStyle"); + + b.Property("ObjectStorageSecretKey") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("objectStorageSecretKey"); + + b.Property("ObjectStorageSetPublicRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("objectStorageSetPublicRead"); + + b.Property("ObjectStorageUseProxy") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("objectStorageUseProxy"); + + b.Property("ObjectStorageUseSsl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("objectStorageUseSSL"); + + b.Property("PinnedClipId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("pinnedClipId"); + + b.Property>("PinnedPages") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(512)[]") + .HasColumnName("pinnedPages") + .HasDefaultValueSql("'{/featured,/channels,/explore,/pages,/about-iceshrimp}'::character varying[]"); + + b.Property>("PinnedUsers") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("pinnedUsers") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("PrivateMode") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("privateMode"); + + b.Property("RecaptchaSecretKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("recaptchaSecretKey"); + + b.Property("RecaptchaSiteKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("recaptchaSiteKey"); + + b.Property>("RecommendedInstances") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("recommendedInstances") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("RemoteDriveCapacityMb") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(32) + .HasColumnName("remoteDriveCapacityMb") + .HasComment("Drive capacity of a remote user (MB)"); + + b.Property("RepositoryUrl") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("repositoryUrl") + .HasDefaultValueSql("'https://iceshrimp.dev/iceshrimp/iceshrimp'::character varying"); + + b.Property("SecureMode") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("secureMode"); + + b.Property>("SilencedHosts") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("silencedHosts") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("SmtpHost") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("smtpHost"); + + b.Property("SmtpPass") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("smtpPass"); + + b.Property("SmtpPort") + .HasColumnType("integer") + .HasColumnName("smtpPort"); + + b.Property("SmtpSecure") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("smtpSecure"); + + b.Property("SmtpUser") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("smtpUser"); + + b.Property("SummalyProxy") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("summalyProxy"); + + b.Property("SwPrivateKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("swPrivateKey"); + + b.Property("SwPublicKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("swPublicKey"); + + b.Property("ThemeColor") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("themeColor"); + + b.Property("ToSurl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("ToSUrl"); + + b.Property("UseObjectStorage") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("useObjectStorage"); + + b.HasKey("Id"); + + b.ToTable("meta"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ModerationLog", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the ModerationLog."); + + b.Property("Info") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("info"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("type"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("moderation_log"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Muting", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Muting."); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiresAt"); + + b.Property("MuteeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("muteeId") + .HasComment("The mutee user ID."); + + b.Property("MuterId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("muterId") + .HasComment("The muter user ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("MuteeId"); + + b.HasIndex("MuterId"); + + b.HasIndex("MuterId", "MuteeId") + .IsUnique(); + + b.ToTable("muting"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Note", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property>("AttachedFileTypes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("attachedFileTypes") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("ChannelId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("channelId") + .HasComment("The ID of source channel."); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Note."); + + b.Property("Cw") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("cw"); + + b.Property>("Emojis") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("emojis") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property>("FileIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("fileIds") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("HasPoll") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("hasPoll"); + + b.Property("LocalOnly") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("localOnly"); + + b.Property>("MentionedRemoteUsers") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("mentionedRemoteUsers") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property>("Mentions") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("mentions") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property>("Reactions") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("reactions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("RenoteCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0) + .HasColumnName("renoteCount"); + + b.Property("RenoteId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("renoteId") + .HasComment("The ID of renote target."); + + b.Property("RenoteUserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("renoteUserHost") + .HasComment("[Denormalized]"); + + b.Property("RenoteUserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("renoteUserId") + .HasComment("[Denormalized]"); + + b.Property("RepliesCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0) + .HasColumnName("repliesCount"); + + b.Property("ReplyId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("replyId") + .HasComment("The ID of reply target."); + + b.Property("ReplyUserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("replyUserHost") + .HasComment("[Denormalized]"); + + b.Property("ReplyUserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("replyUserId") + .HasComment("[Denormalized]"); + + b.Property("Score") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("score"); + + b.Property>("Tags") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("tags") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Text") + .HasColumnType("text") + .HasColumnName("text"); + + b.Property("ThreadId") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("threadId"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the Note."); + + b.Property("Uri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("uri") + .HasComment("The URI of a note. it will be null when the note is local."); + + b.Property("Url") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("url") + .HasComment("The human readable url of a note. it will be null when the note is local."); + + b.Property("UserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("userHost") + .HasComment("[Denormalized]"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The ID of author."); + + b.Property("Visibility") + .HasColumnType("note_visibility_enum") + .HasColumnName("visibility"); + + b.Property>("VisibleUserIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("visibleUserIds") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.HasKey("Id"); + + b.HasIndex("AttachedFileTypes"); + + b.HasIndex("ChannelId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FileIds"); + + b.HasIndex("Mentions"); + + b.HasIndex("RenoteId"); + + b.HasIndex("ReplyId"); + + b.HasIndex("Tags"); + + b.HasIndex("ThreadId"); + + b.HasIndex("Uri") + .IsUnique(); + + b.HasIndex("Url"); + + b.HasIndex("UserHost"); + + b.HasIndex("UserId"); + + b.HasIndex("VisibleUserIds"); + + b.HasIndex("CreatedAt", "UserId"); + + b.HasIndex("Id", "UserHost"); + + b.HasIndex("UserId", "Id"); + + b.HasIndex(new[] { "Text" }, "GIN_TRGM_note_text"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex(new[] { "Text" }, "GIN_TRGM_note_text"), "gin"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex(new[] { "Text" }, "GIN_TRGM_note_text"), new[] { "gin_trgm_ops" }); + + b.HasIndex(new[] { "Mentions" }, "GIN_note_mentions"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex(new[] { "Mentions" }, "GIN_note_mentions"), "gin"); + + b.HasIndex(new[] { "Tags" }, "GIN_note_tags"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex(new[] { "Tags" }, "GIN_note_tags"), "gin"); + + b.HasIndex(new[] { "VisibleUserIds" }, "GIN_note_visibleUserIds"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex(new[] { "VisibleUserIds" }, "GIN_note_visibleUserIds"), "gin"); + + b.ToTable("note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteEdit", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Cw") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("cw"); + + b.Property>("FileIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("fileIds") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId") + .HasComment("The ID of note."); + + b.Property("Text") + .HasColumnType("text") + .HasColumnName("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the Note."); + + b.HasKey("Id"); + + b.HasIndex("NoteId"); + + b.ToTable("note_edit"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteFavorite", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the NoteFavorite."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("NoteId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId") + .IsUnique(); + + b.ToTable("note_favorite"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteReaction", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the NoteReaction."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("Reaction") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)") + .HasColumnName("reaction"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("NoteId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId") + .IsUnique(); + + b.ToTable("note_reaction"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteThreadMuting", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("ThreadId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("threadId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "ThreadId") + .IsUnique(); + + b.ToTable("note_thread_muting"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteUnread", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("IsMentioned") + .HasColumnType("boolean") + .HasColumnName("isMentioned"); + + b.Property("IsSpecified") + .HasColumnType("boolean") + .HasColumnName("isSpecified"); + + b.Property("NoteChannelId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteChannelId") + .HasComment("[Denormalized]"); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("NoteUserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteUserId") + .HasComment("[Denormalized]"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("IsMentioned"); + + b.HasIndex("IsSpecified"); + + b.HasIndex("NoteChannelId"); + + b.HasIndex("NoteId"); + + b.HasIndex("NoteUserId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId") + .IsUnique(); + + b.ToTable("note_unread"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteWatching", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the NoteWatching."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId") + .HasComment("The target Note ID."); + + b.Property("NoteUserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteUserId") + .HasComment("[Denormalized]"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The watcher ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("NoteId"); + + b.HasIndex("NoteUserId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId") + .IsUnique(); + + b.ToTable("note_watching"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Notification", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AppAccessTokenId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("appAccessTokenId"); + + b.Property("Choice") + .HasColumnType("integer") + .HasColumnName("choice"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Notification."); + + b.Property("CustomBody") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("customBody"); + + b.Property("CustomHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("customHeader"); + + b.Property("CustomIcon") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("customIcon"); + + b.Property("FollowRequestId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("followRequestId"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isRead") + .HasComment("Whether the notification was read."); + + b.Property("NoteId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("NotifieeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("notifieeId") + .HasComment("The ID of recipient user of the Notification."); + + b.Property("NotifierId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("notifierId") + .HasComment("The ID of sender user of the Notification."); + + b.Property("Reaction") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("reaction"); + + b.Property("Type") + .HasColumnType("notification_type_enum") + .HasColumnName("type") + .HasComment("The type of the Notification."); + + b.Property("UserGroupInvitationId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userGroupInvitationId"); + + b.HasKey("Id"); + + b.HasIndex("AppAccessTokenId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FollowRequestId"); + + b.HasIndex("IsRead"); + + b.HasIndex("NoteId"); + + b.HasIndex("NotifieeId"); + + b.HasIndex("NotifierId"); + + b.HasIndex("Type"); + + b.HasIndex("UserGroupInvitationId"); + + b.ToTable("notification"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.OauthApp", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("clientId") + .HasComment("The client id of the OAuth application"); + + b.Property("ClientSecret") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("clientSecret") + .HasComment("The client secret of the OAuth application"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the OAuth application"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the OAuth application"); + + b.Property>("RedirectUris") + .IsRequired() + .HasColumnType("character varying(512)[]") + .HasColumnName("redirectUris") + .HasComment("The redirect URIs of the OAuth application"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("character varying(64)[]") + .HasColumnName("scopes") + .HasComment("The scopes requested by the OAuth application"); + + b.Property("Website") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("website") + .HasComment("The website of the OAuth application"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("oauth_app"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.OauthToken", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active") + .HasComment("Whether or not the token has been activated"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("appId"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("code") + .HasComment("The auth code for the OAuth token"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the OAuth token"); + + b.Property("RedirectUri") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("redirectUri") + .HasComment("The redirect URI of the OAuth token"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("character varying(64)[]") + .HasColumnName("scopes") + .HasComment("The scopes requested by the OAuth token"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token") + .HasComment("The OAuth token"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("AppId"); + + b.HasIndex("Code"); + + b.HasIndex("Token"); + + b.HasIndex("UserId"); + + b.ToTable("oauth_token"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Page", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AlignCenter") + .HasColumnType("boolean") + .HasColumnName("alignCenter"); + + b.Property("Content") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("content") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Page."); + + b.Property("EyeCatchingImageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("eyeCatchingImageId"); + + b.Property("Font") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("font"); + + b.Property("HideTitleWhenPinned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("hideTitleWhenPinned"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("isPublic"); + + b.Property("LikedCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("likedCount"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Script") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(16384) + .HasColumnType("character varying(16384)") + .HasColumnName("script") + .HasDefaultValueSql("''::character varying"); + + b.Property("Summary") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("summary"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the Page."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The ID of author."); + + b.Property("Variables") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("variables") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("Visibility") + .HasColumnType("page_visibility_enum") + .HasColumnName("visibility"); + + b.Property>("VisibleUserIds") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(32)[]") + .HasColumnName("visibleUserIds") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EyeCatchingImageId"); + + b.HasIndex("Name"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("UserId"); + + b.HasIndex("VisibleUserIds"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("page"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PageLike", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("PageId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("pageId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "PageId") + .IsUnique(); + + b.ToTable("page_like"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PasswordResetRequest", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("token"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("password_reset_request"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Poll", b => + { + b.Property("NoteId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property>("Choices") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(256)[]") + .HasColumnName("choices") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiresAt"); + + b.Property("Multiple") + .HasColumnType("boolean") + .HasColumnName("multiple"); + + b.Property("NoteVisibility") + .HasColumnType("note_visibility_enum") + .HasColumnName("noteVisibility") + .HasComment("[Denormalized]"); + + b.Property("UserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("userHost") + .HasComment("[Denormalized]"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("[Denormalized]"); + + b.Property>("Votes") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("votes"); + + b.HasKey("NoteId"); + + b.HasIndex("UserHost"); + + b.HasIndex("UserId"); + + b.ToTable("poll"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PollVote", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Choice") + .HasColumnType("integer") + .HasColumnName("choice"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the PollVote."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("NoteId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId", "Choice") + .IsUnique(); + + b.ToTable("poll_vote"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PromoNote", b => + { + b.Property("NoteId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiresAt"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("[Denormalized]"); + + b.HasKey("NoteId"); + + b.HasIndex("UserId"); + + b.ToTable("promo_note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PromoRead", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the PromoRead."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("NoteId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId") + .IsUnique(); + + b.ToTable("promo_read"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistrationTicket", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("registration_ticket"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistryItem", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the RegistryItem."); + + b.Property("Domain") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("domain"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key") + .HasComment("The key of the RegistryItem."); + + b.Property>("Scope") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(1024)[]") + .HasColumnName("scope") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the RegistryItem."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.Property("Value") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("value") + .HasDefaultValueSql("'{}'::jsonb") + .HasComment("The value of the RegistryItem."); + + b.HasKey("Id"); + + b.HasIndex("Domain"); + + b.HasIndex("Scope"); + + b.HasIndex("UserId"); + + b.ToTable("registry_item"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Relay", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Inbox") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("inbox"); + + b.Property("Status") + .HasColumnType("relay_status_enum") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("Inbox") + .IsUnique(); + + b.ToTable("relay"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RenoteMuting", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Muting."); + + b.Property("MuteeId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("muteeId") + .HasComment("The mutee user ID."); + + b.Property("MuterId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("muterId") + .HasComment("The muter user ID."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("MuteeId"); + + b.HasIndex("MuterId"); + + b.HasIndex("MuterId", "MuteeId") + .IsUnique(); + + b.ToTable("renote_muting"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active") + .HasComment("Whether or not the token has been activated (i.e. 2fa has been confirmed)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the OAuth token"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token") + .HasComment("The authorization token"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("Token"); + + b.HasIndex("UserId"); + + b.ToTable("session"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Signin", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Signin."); + + b.Property>("Headers") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("headers"); + + b.Property("Ip") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("signin"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.SwSubscription", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Auth") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("auth"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("endpoint"); + + b.Property("Publickey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("publickey"); + + b.Property("SendReadMessage") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("sendReadMessage"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("sw_subscription"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UsedUsername", b => + { + b.Property("Username") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("username"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.HasKey("Username"); + + b.ToTable("used_username"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.User", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property>("AlsoKnownAs") + .HasColumnType("text[]") + .HasColumnName("alsoKnownAs") + .HasComment("URIs the user is known as too"); + + b.Property("AvatarBlurhash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("avatarBlurhash") + .HasComment("The blurhash of the avatar DriveFile"); + + b.Property("AvatarId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("avatarId") + .HasComment("The ID of avatar DriveFile."); + + b.Property("AvatarUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("avatarUrl") + .HasComment("The URL of the avatar DriveFile"); + + b.Property("BannerBlurhash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("bannerBlurhash") + .HasComment("The blurhash of the banner DriveFile"); + + b.Property("BannerId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("bannerId") + .HasComment("The ID of banner DriveFile."); + + b.Property("BannerUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("bannerUrl") + .HasComment("The URL of the banner DriveFile"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the User."); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the User."); + + b.Property("DriveCapacityOverrideMb") + .HasColumnType("integer") + .HasColumnName("driveCapacityOverrideMb") + .HasComment("Overrides user drive capacity limit"); + + b.Property>("Emojis") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("emojis") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Featured") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("featured") + .HasComment("The featured URL of the User. It will be null if the origin of the user is local."); + + b.Property("FollowersCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("followersCount") + .HasComment("The count of followers."); + + b.Property("FollowersUri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("followersUri") + .HasComment("The URI of the user Follower Collection. It will be null if the origin of the user is local."); + + b.Property("FollowingCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("followingCount") + .HasComment("The count of following."); + + b.Property("HideOnlineStatus") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("hideOnlineStatus"); + + b.Property("Host") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("host") + .HasComment("The host of the User. It will be null if the origin of the user is local."); + + b.Property("Inbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("inbox") + .HasComment("The inbox URL of the User. It will be null if the origin of the user is local."); + + b.Property("IsAdmin") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isAdmin") + .HasComment("Whether the User is the admin."); + + b.Property("IsBot") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isBot") + .HasComment("Whether the User is a bot."); + + b.Property("IsCat") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isCat") + .HasComment("Whether the User is a cat."); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isDeleted") + .HasComment("Whether the User is deleted."); + + b.Property("IsExplorable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("isExplorable") + .HasComment("Whether the User is explorable."); + + b.Property("IsLocked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isLocked") + .HasComment("Whether the User is locked."); + + b.Property("IsModerator") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isModerator") + .HasComment("Whether the User is a moderator."); + + b.Property("IsSilenced") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isSilenced") + .HasComment("Whether the User is silenced."); + + b.Property("IsSuspended") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isSuspended") + .HasComment("Whether the User is suspended."); + + b.Property("LastActiveDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastActiveDate"); + + b.Property("LastFetchedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastFetchedAt"); + + b.Property("MovedToUri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("movedToUri") + .HasComment("The URI of the new account of the User"); + + b.Property("NotesCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("notesCount") + .HasComment("The count of notes."); + + b.Property("SharedInbox") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("sharedInbox") + .HasComment("The sharedInbox URL of the User. It will be null if the origin of the user is local."); + + b.Property("SpeakAsCat") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("speakAsCat") + .HasComment("Whether to speak as a cat if isCat."); + + b.Property>("Tags") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("tags") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Token") + .HasMaxLength(16) + .HasColumnType("character(16)") + .HasColumnName("token") + .IsFixedLength() + .HasComment("The native access token of the User. It will be null if the origin of the user is local."); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedAt") + .HasComment("The updated date of the User."); + + b.Property("Uri") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("uri") + .HasComment("The URI of the User. It will be null if the origin of the user is local."); + + b.Property("Username") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("username") + .HasComment("The username of the User."); + + b.Property("UsernameLower") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("usernameLower") + .HasComment("The username (lowercased) of the User."); + + b.HasKey("Id"); + + b.HasIndex("AvatarId") + .IsUnique(); + + b.HasIndex("BannerId") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Host"); + + b.HasIndex("IsAdmin"); + + b.HasIndex("IsExplorable"); + + b.HasIndex("IsModerator"); + + b.HasIndex("LastActiveDate"); + + b.HasIndex("Tags"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("Uri"); + + b.HasIndex("UsernameLower"); + + b.HasIndex("UsernameLower", "Host") + .IsUnique(); + + b.ToTable("user"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroup", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the UserGroup."); + + b.Property("IsPrivate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isPrivate"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The ID of owner."); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId"); + + b.ToTable("user_group"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroupInvitation", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the UserGroupInvitation."); + + b.Property("UserGroupId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userGroupId") + .HasComment("The group ID."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The user ID."); + + b.HasKey("Id"); + + b.HasIndex("UserGroupId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "UserGroupId") + .IsUnique(); + + b.ToTable("user_group_invitation"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroupMember", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the UserGroupMember."); + + b.Property("UserGroupId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userGroupId") + .HasComment("The group ID."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The user ID."); + + b.HasKey("Id"); + + b.HasIndex("UserGroupId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "UserGroupId") + .IsUnique(); + + b.ToTable("user_group_member"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserKeypair", b => + { + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.Property("PrivateKey") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("privateKey"); + + b.Property("PublicKey") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("publicKey"); + + b.HasKey("UserId"); + + b.ToTable("user_keypair"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserList", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the UserList."); + + b.Property("HideFromHomeTl") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("hideFromHomeTl") + .HasComment("Whether posts from list members should be hidden from the home timeline."); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the UserList."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_list"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserListMember", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the UserListMember."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The user ID."); + + b.Property("UserListId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userListId") + .HasComment("The list ID."); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserListId"); + + b.HasIndex("UserId", "UserListId") + .IsUnique(); + + b.ToTable("user_list_member"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserNotePin", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the UserNotePins."); + + b.Property("NoteId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("noteId"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("NoteId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "NoteId") + .IsUnique(); + + b.ToTable("user_note_pin"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserPending", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("password"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("user_pending"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserProfile", b => + { + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.Property("AlwaysMarkNsfw") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("alwaysMarkNsfw"); + + b.Property("AutoAcceptFollowed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("autoAcceptFollowed"); + + b.Property("Birthday") + .HasMaxLength(10) + .HasColumnType("character(10)") + .HasColumnName("birthday") + .IsFixedLength() + .HasComment("The birthday (YYYY-MM-DD) of the User."); + + b.Property("CarefulBot") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("carefulBot"); + + b.Property("ClientData") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("clientData") + .HasDefaultValueSql("'{}'::jsonb") + .HasComment("The client-specific data of the User."); + + b.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("description") + .HasComment("The description (bio) of the User."); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("email") + .HasComment("The email address of the User."); + + b.Property>("EmailNotificationTypes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("emailNotificationTypes") + .HasDefaultValueSql("'[\"follow\", \"receiveFollowRequest\", \"groupInvited\"]'::jsonb"); + + b.Property("EmailVerified") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("emailVerified"); + + b.Property("EmailVerifyCode") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("emailVerifyCode"); + + b.Property("EnableWordMute") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("enableWordMute"); + + b.Property("FFVisibility") + .ValueGeneratedOnAdd() + .HasColumnType("user_profile_ffvisibility_enum") + .HasDefaultValue(UserProfile.UserProfileFFVisibility.Public) + .HasColumnName("ffVisibility"); + + b.Property("Fields") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("fields") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("InjectFeaturedNote") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("injectFeaturedNote"); + + b.Property("Integrations") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("integrations") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Lang") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("lang"); + + b.Property("Location") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("location") + .HasComment("The location of the User."); + + b.Property>("Mentions") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("mentions") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property("ModerationNote") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("moderationNote") + .HasDefaultValueSql("''::character varying"); + + b.Property>("MutedInstances") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("mutedInstances") + .HasDefaultValueSql("'[]'::jsonb") + .HasComment("List of instances muted by the user."); + + b.Property>>("MutedWords") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("mutedWords") + .HasDefaultValueSql("'[]'::jsonb"); + + b.Property>("MutingNotificationTypes") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("notification_type_enum[]") + .HasColumnName("mutingNotificationTypes") + .HasDefaultValueSql("'{}'::public.notification_type_enum[]"); + + b.Property("NoCrawle") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("noCrawle") + .HasComment("Whether reject index by crawler."); + + b.Property("Password") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("password") + .HasComment("The password hash of the User. It will be null if the origin of the user is local."); + + b.Property("PinnedPageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("pinnedPageId"); + + b.Property("PreventAiLearning") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("preventAiLearning"); + + b.Property("PublicReactions") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("publicReactions"); + + b.Property("ReceiveAnnouncementEmail") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("receiveAnnouncementEmail"); + + b.Property("Room") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("room") + .HasDefaultValueSql("'{}'::jsonb") + .HasComment("The room data of the User."); + + b.Property("SecurityKeysAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("securityKeysAvailable"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("twoFactorEnabled"); + + b.Property("TwoFactorSecret") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("twoFactorSecret"); + + b.Property("TwoFactorTempSecret") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("twoFactorTempSecret"); + + b.Property("Url") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("url") + .HasComment("Remote URL of the user."); + + b.Property("UsePasswordLessLogin") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("usePasswordLessLogin"); + + b.Property("UserHost") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("userHost") + .HasComment("[Denormalized]"); + + b.HasKey("UserId"); + + b.HasIndex("EnableWordMute"); + + b.HasIndex("PinnedPageId") + .IsUnique(); + + b.HasIndex("UserHost"); + + b.ToTable("user_profile"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserPublickey", b => + { + b.Property("UserId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.Property("KeyId") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("keyId"); + + b.Property("KeyPem") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("keyPem"); + + b.HasKey("UserId"); + + b.HasIndex("KeyId") + .IsUnique(); + + b.ToTable("user_publickey"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserSecurityKey", b => + { + b.Property("Id") + .HasColumnType("character varying") + .HasColumnName("id") + .HasComment("Variable-length id given to navigator.credentials.get()"); + + b.Property("LastUsed") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastUsed") + .HasComment("The date of the last time the UserSecurityKey was successfully validated."); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)") + .HasColumnName("name") + .HasComment("User-defined name for this key"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("publicKey") + .HasComment("Variable-length public key used to verify attestations (hex-encoded)."); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("PublicKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_security_key"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Webhook", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("active"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdAt") + .HasComment("The created date of the Antenna."); + + b.Property("LatestSentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("latestSentAt"); + + b.Property("LatestStatus") + .HasColumnType("integer") + .HasColumnName("latestStatus"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("name") + .HasComment("The name of the Antenna."); + + b.Property>("On") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("character varying(128)[]") + .HasColumnName("on") + .HasDefaultValueSql("'{}'::character varying[]"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("secret"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("url"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("userId") + .HasComment("The owner ID."); + + b.HasKey("Id"); + + b.HasIndex("Active"); + + b.HasIndex("On"); + + b.HasIndex("UserId"); + + b.ToTable("webhook"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AbuseUserReport", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Assignee") + .WithMany("AbuseUserReportAssignees") + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Reporter") + .WithMany("AbuseUserReportReporters") + .HasForeignKey("ReporterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "TargetUser") + .WithMany("AbuseUserReportTargetUsers") + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Assignee"); + + b.Navigation("Reporter"); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AccessToken", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.App", "App") + .WithMany("AccessTokens") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("AccessTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("App"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AnnouncementRead", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Announcement", "Announcement") + .WithMany("AnnouncementReads") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("AnnouncementReads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Antenna", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserGroupMember", "UserGroupMember") + .WithMany("Antennas") + .HasForeignKey("UserGroupMemberId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Antennas") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserList", "UserList") + .WithMany("Antennas") + .HasForeignKey("UserListId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + + b.Navigation("UserGroupMember"); + + b.Navigation("UserList"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.App", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Apps") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AttestationChallenge", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("AttestationChallenges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AuthSession", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.App", "App") + .WithMany("AuthSessions") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("AuthSessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("App"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Blocking", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Blockee") + .WithMany("IncomingBlocks") + .HasForeignKey("BlockeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Blocker") + .WithMany("OutgoingBlocks") + .HasForeignKey("BlockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Blockee"); + + b.Navigation("Blocker"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Channel", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFile", "Banner") + .WithMany("Channels") + .HasForeignKey("BannerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Channels") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Banner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ChannelFollowing", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Channel", "Followee") + .WithMany("ChannelFollowings") + .HasForeignKey("FolloweeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Follower") + .WithMany("ChannelFollowings") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Followee"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ChannelNotePin", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Channel", "Channel") + .WithMany("ChannelNotePins") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("ChannelNotePins") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("Note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Clip", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Clips") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ClipNote", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Clip", "Clip") + .WithMany("ClipNotes") + .HasForeignKey("ClipId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("ClipNotes") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clip"); + + b.Navigation("Note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.DriveFile", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFolder", "Folder") + .WithMany("DriveFiles") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("DriveFiles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Folder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.DriveFolder", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFolder", "Parent") + .WithMany("InverseParent") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("DriveFolders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Parent"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.FollowRequest", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Followee") + .WithMany("IncomingFollowRequests") + .HasForeignKey("FolloweeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Follower") + .WithMany("OutgoingFollowRequests") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Followee"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Following", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Followee") + .WithMany("IncomingFollowRelationships") + .HasForeignKey("FolloweeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Follower") + .WithMany("OutgoingFollowRelationships") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Followee"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.GalleryLike", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.GalleryPost", "Post") + .WithMany("GalleryLikes") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("GalleryLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.GalleryPost", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("GalleryPosts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.HtmlNoteCacheEntry", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithOne("HtmlNoteCacheEntry") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.HtmlNoteCacheEntry", "NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.HtmlUserCacheEntry", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithOne("HtmlUserCacheEntry") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.HtmlUserCacheEntry", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.MessagingMessage", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFile", "File") + .WithMany("MessagingMessages") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserGroup", "Group") + .WithMany("MessagingMessages") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Recipient") + .WithMany("MessagingMessageRecipients") + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("MessagingMessageUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Group"); + + b.Navigation("Recipient"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.ModerationLog", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("ModerationLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Muting", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Mutee") + .WithMany("IncomingMutes") + .HasForeignKey("MuteeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Muter") + .WithMany("OutgoingMutes") + .HasForeignKey("MuterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Mutee"); + + b.Navigation("Muter"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Note", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Channel", "Channel") + .WithMany("Notes") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Renote") + .WithMany("InverseRenote") + .HasForeignKey("RenoteId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Reply") + .WithMany("InverseReply") + .HasForeignKey("ReplyId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Notes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("Renote"); + + b.Navigation("Reply"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteEdit", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("NoteEdits") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteFavorite", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("NoteFavorites") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("NoteFavorites") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteReaction", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("NoteReactions") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("NoteReactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteThreadMuting", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("NoteThreadMutings") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteUnread", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("NoteUnreads") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("NoteUnreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.NoteWatching", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("NoteWatchings") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("NoteWatchings") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Notification", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.AccessToken", "AppAccessToken") + .WithMany("Notifications") + .HasForeignKey("AppAccessTokenId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.FollowRequest", "FollowRequest") + .WithMany("Notifications") + .HasForeignKey("FollowRequestId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("Notifications") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Notifiee") + .WithMany("NotificationNotifiees") + .HasForeignKey("NotifieeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Notifier") + .WithMany("NotificationNotifiers") + .HasForeignKey("NotifierId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserGroupInvitation", "UserGroupInvitation") + .WithMany("Notifications") + .HasForeignKey("UserGroupInvitationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("AppAccessToken"); + + b.Navigation("FollowRequest"); + + b.Navigation("Note"); + + b.Navigation("Notifiee"); + + b.Navigation("Notifier"); + + b.Navigation("UserGroupInvitation"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.OauthToken", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.OauthApp", "App") + .WithMany("OauthTokens") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("OauthTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("App"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Page", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFile", "EyeCatchingImage") + .WithMany("Pages") + .HasForeignKey("EyeCatchingImageId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Pages") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EyeCatchingImage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PageLike", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Page", "Page") + .WithMany("PageLikes") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("PageLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Page"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PasswordResetRequest", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("PasswordResetRequests") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Poll", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithOne("Poll") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.Poll", "NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PollVote", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("PollVotes") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("PollVotes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PromoNote", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithOne("PromoNote") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.PromoNote", "NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.PromoRead", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("PromoReads") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("PromoReads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistryItem", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("RegistryItems") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RenoteMuting", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Mutee") + .WithMany("RenoteMutingMutees") + .HasForeignKey("MuteeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "Muter") + .WithMany("RenoteMutingMuters") + .HasForeignKey("MuterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Mutee"); + + b.Navigation("Muter"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Session", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Signin", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Signins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.SwSubscription", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("SwSubscriptions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.User", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFile", "Avatar") + .WithOne("UserAvatar") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.User", "AvatarId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.DriveFile", "Banner") + .WithOne("UserBanner") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.User", "BannerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Avatar"); + + b.Navigation("Banner"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroup", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserGroups") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroupInvitation", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserGroup", "UserGroup") + .WithMany("UserGroupInvitations") + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserGroupInvitations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserGroup"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroupMember", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserGroup", "UserGroup") + .WithMany("UserGroupMembers") + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserGroupMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserGroup"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserKeypair", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithOne("UserKeypair") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.UserKeypair", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserList", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserListMember", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserListMembers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.UserList", "UserList") + .WithMany("UserListMembers") + .HasForeignKey("UserListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserList"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserNotePin", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Note", "Note") + .WithMany("UserNotePins") + .HasForeignKey("NoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserNotePins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Note"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserProfile", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.Page", "PinnedPage") + .WithOne("UserProfile") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.UserProfile", "PinnedPageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithOne("UserProfile") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.UserProfile", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PinnedPage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserPublickey", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithOne("UserPublickey") + .HasForeignKey("Iceshrimp.Backend.Core.Database.Tables.UserPublickey", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserSecurityKey", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("UserSecurityKeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Webhook", b => + { + b.HasOne("Iceshrimp.Backend.Core.Database.Tables.User", "User") + .WithMany("Webhooks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.AccessToken", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Announcement", b => + { + b.Navigation("AnnouncementReads"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.App", b => + { + b.Navigation("AccessTokens"); + + b.Navigation("AuthSessions"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Channel", b => + { + b.Navigation("ChannelFollowings"); + + b.Navigation("ChannelNotePins"); + + b.Navigation("Notes"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Clip", b => + { + b.Navigation("ClipNotes"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.DriveFile", b => + { + b.Navigation("Channels"); + + b.Navigation("MessagingMessages"); + + b.Navigation("Pages"); + + b.Navigation("UserAvatar"); + + b.Navigation("UserBanner"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.DriveFolder", b => + { + b.Navigation("DriveFiles"); + + b.Navigation("InverseParent"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.FollowRequest", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.GalleryPost", b => + { + b.Navigation("GalleryLikes"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Note", b => + { + b.Navigation("ChannelNotePins"); + + b.Navigation("ClipNotes"); + + b.Navigation("HtmlNoteCacheEntry"); + + b.Navigation("InverseRenote"); + + b.Navigation("InverseReply"); + + b.Navigation("NoteEdits"); + + b.Navigation("NoteFavorites"); + + b.Navigation("NoteReactions"); + + b.Navigation("NoteUnreads"); + + b.Navigation("NoteWatchings"); + + b.Navigation("Notifications"); + + b.Navigation("Poll"); + + b.Navigation("PollVotes"); + + b.Navigation("PromoNote"); + + b.Navigation("PromoReads"); + + b.Navigation("UserNotePins"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.OauthApp", b => + { + b.Navigation("OauthTokens"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.Page", b => + { + b.Navigation("PageLikes"); + + b.Navigation("UserProfile"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.User", b => + { + b.Navigation("AbuseUserReportAssignees"); + + b.Navigation("AbuseUserReportReporters"); + + b.Navigation("AbuseUserReportTargetUsers"); + + b.Navigation("AccessTokens"); + + b.Navigation("AnnouncementReads"); + + b.Navigation("Antennas"); + + b.Navigation("Apps"); + + b.Navigation("AttestationChallenges"); + + b.Navigation("AuthSessions"); + + b.Navigation("ChannelFollowings"); + + b.Navigation("Channels"); + + b.Navigation("Clips"); + + b.Navigation("DriveFiles"); + + b.Navigation("DriveFolders"); + + b.Navigation("GalleryLikes"); + + b.Navigation("GalleryPosts"); + + b.Navigation("HtmlUserCacheEntry"); + + b.Navigation("IncomingBlocks"); + + b.Navigation("IncomingFollowRelationships"); + + b.Navigation("IncomingFollowRequests"); + + b.Navigation("IncomingMutes"); + + b.Navigation("MessagingMessageRecipients"); + + b.Navigation("MessagingMessageUsers"); + + b.Navigation("ModerationLogs"); + + b.Navigation("NoteFavorites"); + + b.Navigation("NoteReactions"); + + b.Navigation("NoteThreadMutings"); + + b.Navigation("NoteUnreads"); + + b.Navigation("NoteWatchings"); + + b.Navigation("Notes"); + + b.Navigation("NotificationNotifiees"); + + b.Navigation("NotificationNotifiers"); + + b.Navigation("OauthTokens"); + + b.Navigation("OutgoingBlocks"); + + b.Navigation("OutgoingFollowRelationships"); + + b.Navigation("OutgoingFollowRequests"); + + b.Navigation("OutgoingMutes"); + + b.Navigation("PageLikes"); + + b.Navigation("Pages"); + + b.Navigation("PasswordResetRequests"); + + b.Navigation("PollVotes"); + + b.Navigation("PromoReads"); + + b.Navigation("RegistryItems"); + + b.Navigation("RenoteMutingMutees"); + + b.Navigation("RenoteMutingMuters"); + + b.Navigation("Sessions"); + + b.Navigation("Signins"); + + b.Navigation("SwSubscriptions"); + + b.Navigation("UserGroupInvitations"); + + b.Navigation("UserGroupMemberships"); + + b.Navigation("UserGroups"); + + b.Navigation("UserKeypair"); + + b.Navigation("UserListMembers"); + + b.Navigation("UserLists"); + + b.Navigation("UserNotePins"); + + b.Navigation("UserProfile"); + + b.Navigation("UserPublickey"); + + b.Navigation("UserSecurityKeys"); + + b.Navigation("Webhooks"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroup", b => + { + b.Navigation("MessagingMessages"); + + b.Navigation("UserGroupInvitations"); + + b.Navigation("UserGroupMembers"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroupInvitation", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserGroupMember", b => + { + b.Navigation("Antennas"); + }); + + modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.UserList", b => + { + b.Navigation("Antennas"); + + b.Navigation("UserListMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240210233409_FixNoteMentionsColumnType.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240210233409_FixNoteMentionsColumnType.cs new file mode 100644 index 00000000..d534b488 --- /dev/null +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240210233409_FixNoteMentionsColumnType.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Iceshrimp.Backend.Core.Database.Tables; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Iceshrimp.Backend.Core.Database.Migrations +{ + /// + public partial class FixNoteMentionsColumnType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "note" ALTER COLUMN "mentionedRemoteUsers" DROP DEFAULT; + ALTER TABLE "note" ALTER COLUMN "mentionedRemoteUsers" TYPE jsonb USING "mentionedRemoteUsers"::jsonb; + ALTER TABLE "note" ALTER COLUMN "mentionedRemoteUsers" SET DEFAULT '[]'::jsonb; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + ALTER TABLE "note" ALTER COLUMN "mentionedRemoteUsers" DROP DEFAULT; + ALTER TABLE "note" ALTER COLUMN "mentionedRemoteUsers" TYPE text USING "mentionedRemoteUsers"::text; + ALTER TABLE "note" ALTER COLUMN "mentionedRemoteUsers" SET DEFAULT '[]'::text; + """); + } + } +} diff --git a/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs b/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs index f95129f2..4ec5282e 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -2417,12 +2417,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasDefaultValue(false) .HasColumnName("localOnly"); - b.Property("MentionedRemoteUsers") + b.Property>("MentionedRemoteUsers") .IsRequired() .ValueGeneratedOnAdd() - .HasColumnType("text") + .HasColumnType("jsonb") .HasColumnName("mentionedRemoteUsers") - .HasDefaultValueSql("'[]'::text"); + .HasDefaultValueSql("'[]'::jsonb"); b.Property>("Mentions") .IsRequired() @@ -4451,7 +4451,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("location") .HasComment("The location of the User."); - b.Property>("Mentions") + b.Property>("Mentions") .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("jsonb") diff --git a/Iceshrimp.Backend/Core/Database/Tables/Note.cs b/Iceshrimp.Backend/Core/Database/Tables/Note.cs index 8f342d5e..69c8ed97 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/Note.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/Note.cs @@ -5,6 +5,7 @@ using EntityFrameworkCore.Projectables; using Iceshrimp.Backend.Core.Helpers; using Microsoft.EntityFrameworkCore; using NpgsqlTypes; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Iceshrimp.Backend.Core.Database.Tables; @@ -106,7 +107,8 @@ public class Note : IEntity { [Column("mentions", TypeName = "character varying(32)[]")] public List Mentions { get; set; } = []; - [Column("mentionedRemoteUsers")] public string MentionedRemoteUsers { get; set; } = null!; + [Column("mentionedRemoteUsers", TypeName = "jsonb")] + public List MentionedRemoteUsers { get; set; } = []; [Column("emojis", TypeName = "character varying(128)[]")] public List Emojis { get; set; } = []; @@ -271,4 +273,11 @@ public class Note : IEntity { return this; } + + public class MentionedUser { + [J("uri")] public required string Uri { get; set; } + [J("url")] public string? Url { get; set; } + [J("username")] public required string Username { get; set; } + [J("host")] public required string Host { get; set; } + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs b/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs index ee11de2c..f0066fb6 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/UserProfile.cs @@ -157,7 +157,7 @@ public class UserProfile { [Column("preventAiLearning")] public bool PreventAiLearning { get; set; } [Column("mentions", TypeName = "jsonb")] - public List Mentions { get; set; } = null!; + public List Mentions { get; set; } = null!; [ForeignKey("PinnedPageId")] [InverseProperty(nameof(Page.UserProfile))] @@ -179,11 +179,4 @@ public class UserProfile { [J("value")] public required string Value { get; set; } [J("verified")] public bool? IsVerified { get; set; } } - - public class MentionedRemoteUsers { - [J("uri")] public required string Uri { get; set; } - [J("url")] public string? Url { get; set; } - [J("username")] public required string Username { get; set; } - [J("host")] public required string Host { get; set; } - } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/EnumerableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/EnumerableExtensions.cs index e69bcc9f..d2005381 100644 --- a/Iceshrimp.Backend/Core/Extensions/EnumerableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/EnumerableExtensions.cs @@ -4,4 +4,13 @@ public static class EnumerableExtensions { public static async Task> AwaitAllAsync(this IEnumerable> tasks) { return await Task.WhenAll(tasks); } + + public static async Task> AwaitAllNoConcurrencyAsync(this IEnumerable> tasks) { + var results = new List(); + foreach (var task in tasks) { + results.Add(await task); + } + + return results; + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index 71913a11..aba6e2ca 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -4,6 +4,7 @@ using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Federation.WebFinger; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Services; using Microsoft.AspNetCore.DataProtection; @@ -27,6 +28,7 @@ public static class ServiceExtensions { .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() @@ -46,6 +48,7 @@ public static class ServiceExtensions { // Singleton = instantiated once across application lifetime services .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs new file mode 100644 index 00000000..765c7277 --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/MentionsResolver.cs @@ -0,0 +1,83 @@ +using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Extensions; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Types; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; + +namespace Iceshrimp.Backend.Core.Federation.ActivityPub; + +using SplitDomainMapping = IReadOnlyDictionary<(string usernameLower, string webDomain), string>; + +/// +/// Resolves mentions into their canonical form. This is required for handling split domain mentions correctly, as it cannot be guaranteed that remote instances handle split domain users correctly. +/// +public class MentionsResolver( + DatabaseContext db, + IOptions config, + IDistributedCache cache +) { + public async Task ResolveMentions( + string mfm, string? host, + List mentionCache, + SplitDomainMapping splitDomainMapping + ) { + var nodes = MfmParser.Parse(mfm); + nodes = await ResolveMentions(nodes, host, mentionCache, splitDomainMapping); + return MfmSerializer.Serialize(nodes); + } + + public async Task> ResolveMentions( + IEnumerable nodes, string? host, + List mentionCache, + SplitDomainMapping splitDomainMapping + ) { + var nodesList = nodes.ToList(); + foreach (var mention in nodesList.SelectMany(p => p.Children.Append(p)).OfType()) + await ResolveMention(mention, host, mentionCache, splitDomainMapping); + + return nodesList; + } + + private async Task ResolveMention( + MfmMentionNode node, string? host, + IEnumerable mentionCache, + SplitDomainMapping splitDomainMapping + ) { + var finalHost = node.Host ?? host; + + if (finalHost == config.Value.AccountDomain || finalHost == config.Value.WebDomain) + finalHost = null; + if (finalHost != null && + splitDomainMapping.TryGetValue((node.Username.ToLowerInvariant(), finalHost), out var value)) + finalHost = value; + + var resolvedUser = + mentionCache.FirstOrDefault(p => string.Equals(p.Username, node.Username, + StringComparison.InvariantCultureIgnoreCase) && + p.Host == finalHost); + + if (resolvedUser != null) { + node.Username = resolvedUser.Username; + node.Host = resolvedUser.Host; + node.Acct = $"@{resolvedUser.Username}@{resolvedUser.Host}"; + } + else { + async Task FetchLocalUserCapitalization() { + var username = await db.Users.Where(p => p.UsernameLower == node.Username.ToLowerInvariant()) + .Select(p => p.Username) + .FirstOrDefaultAsync(); + return username ?? node.Username; + } + + node.Username = await cache.FetchAsync($"localUserNameCapitalization:{node.Username.ToLowerInvariant()}", + TimeSpan.FromHours(24), FetchLocalUserCapitalization); + + node.Acct = $"@{node.Username}"; + } + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs index d15475b9..a695b6e8 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs @@ -6,30 +6,30 @@ using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Core.Federation.ActivityPub; -public class NoteRenderer(IOptions config) { +public class NoteRenderer(IOptions config, MfmConverter mfmConverter) { public async Task RenderAsync(Note note) { var id = $"https://{config.Value.WebDomain}/notes/{note.Id}"; var userId = $"https://{config.Value.WebDomain}/users/{note.User.Id}"; var replyId = note.ReplyId != null - ? new ASIdObject($"https://{config.Value.WebDomain}/notes/{note.ReplyId}") + ? new ASObjectBase($"https://{config.Value.WebDomain}/notes/{note.ReplyId}") : null; - List to = note.Visibility switch { + List to = note.Visibility switch { Note.NoteVisibility.Public => [new ASLink($"{Constants.ActivityStreamsNs}#Public")], Note.NoteVisibility.Followers => [new ASLink($"{userId}/followers")], Note.NoteVisibility.Specified => [], // FIXME _ => [] }; - List cc = note.Visibility switch { + List cc = note.Visibility switch { Note.NoteVisibility.Home => [new ASLink($"{Constants.ActivityStreamsNs}#Public")], _ => [] }; return new ASNote { Id = id, - Content = note.Text != null ? await MfmConverter.ToHtmlAsync(note.Text) : null, - AttributedTo = [new ASIdObject(userId)], + Content = note.Text != null ? await mfmConverter.ToHtmlAsync(note.Text, []) : null, + AttributedTo = [new ASObjectBase(userId)], Type = $"{Constants.ActivityStreamsNs}#Note", MkContent = note.Text, PublishedAt = note.CreatedAt, diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs index 88d16b79..70cb95b3 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ObjectResolver.cs @@ -10,29 +10,29 @@ public class ObjectResolver( DatabaseContext db, FederationControlService federationCtrl ) { - public async Task ResolveObject(ASIdObject idObj) { - if (idObj is ASObject obj) return obj; - if (idObj.Id == null) { + public async Task ResolveObject(ASObjectBase baseObj) { + if (baseObj is ASObject obj) return obj; + if (baseObj.Id == null) { logger.LogDebug("Refusing to resolve object with null id property"); return null; } - if (await federationCtrl.ShouldBlockAsync(idObj.Id)) { + if (await federationCtrl.ShouldBlockAsync(baseObj.Id)) { logger.LogDebug("Instance is blocked"); return null; } - if (await db.Notes.AnyAsync(p => p.Uri == idObj.Id)) - return new ASNote { Id = idObj.Id }; - if (await db.Users.AnyAsync(p => p.Uri == idObj.Id)) - return new ASActor { Id = idObj.Id }; + if (await db.Notes.AnyAsync(p => p.Uri == baseObj.Id)) + return new ASNote { Id = baseObj.Id }; + if (await db.Users.AnyAsync(p => p.Uri == baseObj.Id)) + return new ASActor { Id = baseObj.Id }; try { - var result = await fetchSvc.FetchActivityAsync(idObj.Id); + var result = await fetchSvc.FetchActivityAsync(baseObj.Id); return result.FirstOrDefault(); } catch (Exception e) { - logger.LogDebug("Failed to resolve object {id}: {error}", idObj.Id, e); + logger.LogDebug("Failed to resolve object {id}: {error}", baseObj.Id, e); return null; } } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs index 01e467a2..9adea62b 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserRenderer.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Core.Federation.ActivityPub; -public class UserRenderer(IOptions config, DatabaseContext db) { +public class UserRenderer(IOptions config, DatabaseContext db, MfmConverter mfmConverter) { /// /// This function is meant for compacting an actor into the @id form as specified in ActivityStreams /// @@ -58,17 +58,17 @@ public class UserRenderer(IOptions config, DatabaseConte Url = new ASLink($"https://{config.Value.WebDomain}/@{user.Username}"), Username = user.Username, DisplayName = user.DisplayName ?? user.Username, - Summary = profile?.Description != null ? await MfmConverter.FromHtmlAsync(profile.Description) : null, + Summary = profile?.Description != null ? await mfmConverter.FromHtmlAsync(profile.Description) : null, MkSummary = profile?.Description, IsCat = user.IsCat, IsDiscoverable = user.IsExplorable, IsLocked = user.IsLocked, Endpoints = new ASEndpoints { - SharedInbox = new ASIdObject($"https://{config.Value.WebDomain}/inbox") + SharedInbox = new ASObjectBase($"https://{config.Value.WebDomain}/inbox") }, PublicKey = new ASPublicKey { Id = $"{id}#main-key", - Owner = new ASIdObject(id), + Owner = new ASObjectBase(id), PublicKey = keypair.PublicKey, Type = "Key" } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs index c43e1f40..46ac24da 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs @@ -71,6 +71,10 @@ public class UserResolver(ILogger logger, UserService userSvc, Web return query; } + public async Task ResolveAsync(string username, string? host) { + return host != null ? await ResolveAsync($"acct:{username}@{host}") : await ResolveAsync($"acct:{username}"); + } + public async Task ResolveAsync(string query) { query = NormalizeQuery(query); diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASCollection.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASCollection.cs index 3d4b3139..b823fd83 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASCollection.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASCollection.cs @@ -6,7 +6,7 @@ using VC = Iceshrimp.Backend.Core.Federation.ActivityStreams.Types.ValueObjectCo namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; -public class ASCollection() : ASIdObject where T : ASObject { +public class ASCollection() : ASObjectBase where T : ASObject { public ASCollection(string id) : this() { Id = id; } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASEndpoints.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASEndpoints.cs index 05cdd4ab..a90f6b32 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASEndpoints.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASEndpoints.cs @@ -5,8 +5,8 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; public class ASEndpoints { [J("https://www.w3.org/ns/activitystreams#sharedInbox")] - [JC(typeof(LDIdObjectConverter))] - public ASIdObject? SharedInbox { get; set; } + [JC(typeof(ASObjectBaseConverter))] + public ASObjectBase? SharedInbox { get; set; } } public class ASEndpointsConverter : ASSerializer.ListSingleObjectConverter; \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASLink.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASLink.cs index f6abe692..2ff64b80 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASLink.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASLink.cs @@ -1,12 +1,18 @@ using J = Newtonsoft.Json.JsonPropertyAttribute; +using JC = Newtonsoft.Json.JsonConverterAttribute; namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; -public class ASLink(string url) : ASIdObject(url) { +public class ASLink(string url) : ASObjectBase(url) { [J("https://www.w3.org/ns/activitystreams#href")] - public string? Href { get; set; } + [JC(typeof(ASObjectBaseConverter))] + public ASObjectBase? Href { get; set; } - public string? Link => Id ?? Href; + [J("https://www.w3.org/ns/activitystreams#name")] + [JC(typeof(ValueObjectConverter))] + public string? Name { get; set; } + + public string? Link => Id ?? Href?.Id; public override string? ToString() => Link; } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs index f3c178b8..a8f9e25b 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs @@ -32,17 +32,21 @@ public class ASNote : ASObject { public ASNoteSource? Source { get; set; } [J("https://www.w3.org/ns/activitystreams#to")] - public List To { get; set; } = []; + public List To { get; set; } = []; [J("https://www.w3.org/ns/activitystreams#cc")] - public List Cc { get; set; } = []; + public List Cc { get; set; } = []; [J("https://www.w3.org/ns/activitystreams#attributedTo")] - public List AttributedTo { get; set; } = []; + public List AttributedTo { get; set; } = []; [J("https://www.w3.org/ns/activitystreams#inReplyTo")] - [JC(typeof(LDIdObjectConverter))] - public ASIdObject? InReplyTo { get; set; } + [JC(typeof(ASObjectBaseConverter))] + public ASObjectBase? InReplyTo { get; set; } + + [J("https://www.w3.org/ns/activitystreams#tag")] + [JC(typeof(ASTagConverter))] + public List? Tags { get; set; } public Note.NoteVisibility GetVisibility(ASActor actor) { if (To.Any(p => p.Id == "https://www.w3.org/ns/activitystreams#Public")) @@ -55,6 +59,17 @@ public class ASNote : ASObject { return Note.NoteVisibility.Specified; } + public List GetRecipients(ASActor actor) { + return To.Concat(Cc) + .Select(p => p.Id) + .Distinct() + .Where(p => p != $"{Constants.ActivityStreamsNs}#Public" && + p != (actor.Followers?.Id ?? actor.Id + "/followers")) + .Where(p => p != null) + .Select(p => p!) + .ToList(); + } + public new static class Types { private const string Ns = Constants.ActivityStreamsNs; diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs index 008e40e4..7c17bac7 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObject.cs @@ -7,11 +7,11 @@ using JR = Newtonsoft.Json.JsonRequiredAttribute; namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; -public class ASObject : ASIdObject { +public class ASObject : ASObjectBase { [J("@id")] [JR] public new required string Id { get; set; } [J("@type")] - [JC(typeof(LDTypeConverter))] + [JC(typeof(StringListSingleConverter))] public string? Type { get; set; } public bool IsUnresolved => GetType() == typeof(ASObject) && Type == null; @@ -55,7 +55,7 @@ public class ASObject : ASIdObject { public class ASTombstone : ASObject; -public sealed class LDTypeConverter : ASSerializer.ListSingleObjectConverter; +public sealed class StringListSingleConverter : ASSerializer.ListSingleObjectConverter; internal sealed class ASObjectConverter : JsonConverter { public override bool CanWrite => false; diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASIdObject.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObjectBase.cs similarity index 57% rename from Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASIdObject.cs rename to Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObjectBase.cs index 41423338..516d64a9 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASIdObject.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASObjectBase.cs @@ -2,8 +2,8 @@ using J = Newtonsoft.Json.JsonPropertyAttribute; namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; -public class ASIdObject() { - public ASIdObject(string? id) : this() { +public class ASObjectBase() { + public ASObjectBase(string? id) : this() { Id = id; } @@ -14,4 +14,4 @@ public class ASIdObject() { } } -public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter; \ No newline at end of file +public sealed class ASObjectBaseConverter : ASSerializer.ListSingleObjectConverter; \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASPublicKey.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASPublicKey.cs index 2fb9dc47..c7493121 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASPublicKey.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASPublicKey.cs @@ -6,8 +6,8 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; public class ASPublicKey : ASObject { [J("https://w3id.org/security#owner")] - [JC(typeof(LDIdObjectConverter))] - public ASIdObject? Owner { get; set; } + [JC(typeof(ASObjectBaseConverter))] + public ASObjectBase? Owner { get; set; } [J("https://w3id.org/security#publicKeyPem")] [JC(typeof(VC))] diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASTag.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASTag.cs new file mode 100644 index 00000000..57d709da --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASTag.cs @@ -0,0 +1,75 @@ +using Iceshrimp.Backend.Core.Configuration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using J = Newtonsoft.Json.JsonPropertyAttribute; +using JC = Newtonsoft.Json.JsonConverterAttribute; + +namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; + +public class ASTag : ASObjectBase { + [J("@type")] + [JC(typeof(StringListSingleConverter))] + public string? Type { get; set; } +} + +public class ASTagLink : ASTag { + [J("https://www.w3.org/ns/activitystreams#href")] + [JC(typeof(ASObjectBaseConverter))] + public ASObjectBase? Href { get; set; } + + [J("https://www.w3.org/ns/activitystreams#name")] + [JC(typeof(ValueObjectConverter))] + public string? Name { get; set; } +} + +public class ASMention : ASTagLink; + +public class ASHashtag : ASTagLink; + +public class ASEmoji : ASTag { + //TODO +} + +public sealed class ASTagConverter : JsonConverter { + public override bool CanWrite => false; + + public override bool CanConvert(Type objectType) { + return true; + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) { + if (reader.TokenType == JsonToken.StartObject) { + var obj = JObject.Load(reader); + return HandleObject(obj); + } + + if (reader.TokenType == JsonToken.StartArray) { + var array = JArray.Load(reader); + var result = new List(); + foreach (var token in array) { + if (token is not JObject obj) return null; + var item = HandleObject(obj); + if (item == null) return null; + result.Add(item); + } + + return result; + } + + return null; + } + + private ASTag? HandleObject(JToken obj) { + var link = obj.ToObject(); + if (link is not { Href: not null }) return obj.ToObject(); + + return link.Type == $"{Constants.ActivityStreamsNs}#Mention" + ? obj.ToObject() + : obj.ToObject(); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs index 86abd523..2fae66ae 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs @@ -4,15 +4,18 @@ using System.Web; using AngleSharp; using AngleSharp.Dom; using AngleSharp.Html.Parser; +using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Types; -using static Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser; +using Microsoft.Extensions.Options; +using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser; using HtmlParser = AngleSharp.Html.Parser.HtmlParser; namespace Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; -public static class MfmConverter { - public static async Task FromHtmlAsync(string? html) { +public class MfmConverter(IOptions config) { + public async Task FromHtmlAsync(string? html, List? mentions = null) { if (html == null) return null; // Ensure compatibility with AP servers that send both
as well as newlines @@ -22,46 +25,50 @@ public static class MfmConverter { var dom = await new HtmlParser().ParseDocumentAsync(html); if (dom.Body == null) return ""; - var sb = new StringBuilder(); - dom.Body.ChildNodes.Select(ParseNode).ToList().ForEach(s => sb.Append(s)); + var sb = new StringBuilder(); + var parser = new MfmHtmlParser(mentions ?? []); + dom.Body.ChildNodes.Select(parser.ParseNode).ToList().ForEach(s => sb.Append(s)); return sb.ToString().Trim(); } - public static async Task ToHtmlAsync(string mfm) { - var nodes = MfmParser.Parse(mfm); - + public async Task ToHtmlAsync(IEnumerable nodes, List mentions) { var context = BrowsingContext.New(); var document = await context.OpenNewAsync(); var element = document.CreateElement("p"); - foreach (var node in nodes) element.AppendNodes(document.FromMfmNode(node)); + foreach (var node in nodes) element.AppendNodes(FromMfmNode(document, node, mentions)); await using var sw = new StringWriter(); await element.ToHtmlAsync(sw); return sw.ToString(); } - private static INode FromMfmNode(this IDocument document, MfmNode node) { + public async Task ToHtmlAsync(string mfm, List mentions) { + var nodes = MfmParser.Parse(mfm); + return await ToHtmlAsync(nodes, mentions); + } + + private INode FromMfmNode(IDocument document, MfmNode node, List mentions) { switch (node) { case MfmBoldNode: { var el = document.CreateElement("b"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmSmallNode: { var el = document.CreateElement("small"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmStrikeNode: { var el = document.CreateElement("del"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmItalicNode: case MfmFnNode: { var el = document.CreateElement("i"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmCodeBlockNode codeBlockNode: { @@ -73,7 +80,7 @@ public static class MfmConverter { } case MfmCenterNode: { var el = document.CreateElement("div"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmEmojiCodeNode emojiCodeNode: { @@ -84,8 +91,7 @@ public static class MfmConverter { } case MfmHashtagNode hashtagNode: { var el = document.CreateElement("a"); - //TODO: get url from config - el.SetAttribute("href", $"https://example.org/tags/{hashtagNode.Hashtag}"); + el.SetAttribute("href", $"https://{config.Value.WebDomain}/tags/{hashtagNode.Hashtag}"); el.TextContent = $"#{hashtagNode.Hashtag}"; el.SetAttribute("rel", "tag"); return el; @@ -108,18 +114,45 @@ public static class MfmConverter { case MfmLinkNode linkNode: { var el = document.CreateElement("a"); el.SetAttribute("href", linkNode.Url); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmMentionNode mentionNode: { var el = document.CreateElement("span"); - el.TextContent = mentionNode.Acct; - //TODO: Resolve mentions and only fall back to the above + + if (mentionNode.Host == config.Value.AccountDomain || mentionNode.Host == config.Value.WebDomain) + mentionNode.Host = null; + + var mention = mentionNode.Host == null + ? new Note.MentionedUser { + Host = config.Value.AccountDomain, + Uri = $"https://{config.Value.WebDomain}/@{mentionNode.Username}", + Username = mentionNode.Username + } + : mentions.FirstOrDefault(p => string.Equals(p.Username, mentionNode.Username, + StringComparison.InvariantCultureIgnoreCase) && + string.Equals(p.Host, mentionNode.Host, + StringComparison.InvariantCultureIgnoreCase)); + if (mention == null) { + el.TextContent = mentionNode.Acct; + } + else { + el.ClassList.Add("h-card"); + el.SetAttribute("translate", "no"); + var a = document.CreateElement("a"); + a.ClassList.Add("u-url", "mention"); + a.SetAttribute("href", mention.Url ?? mention.Uri); + var span = document.CreateElement("span"); + span.TextContent = $"@{mention.Username}"; + a.AppendChild(span); + el.AppendChild(a); + } + return el; } case MfmQuoteNode: { var el = document.CreateElement("blockquote"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } case MfmTextNode textNode: { @@ -155,7 +188,7 @@ public static class MfmConverter { } case MfmPlainNode: { var el = document.CreateElement("span"); - el.AppendChildren(document, node); + AppendChildren(el, document, node, mentions); return el; } default: { @@ -164,7 +197,9 @@ public static class MfmConverter { } } - private static void AppendChildren(this INode element, IDocument document, MfmNode parent) { - foreach (var node in parent.Children) element.AppendNodes(document.FromMfmNode(node)); + private void AppendChildren(INode element, IDocument document, MfmNode parent, + List mentions + ) { + foreach (var node in parent.Children) element.AppendNodes(FromMfmNode(document, node, mentions)); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs index 87d38524..2fac4e0d 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs @@ -1,9 +1,11 @@ using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using Iceshrimp.Backend.Core.Database.Tables; namespace Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; -internal static class HtmlParser { - internal static string? ParseNode(INode node) { +internal class HtmlParser(IEnumerable mentions) { + internal string? ParseNode(INode node) { if (node.NodeType is NodeType.Text) return node.TextContent; if (node.NodeType is NodeType.Comment or NodeType.Document) @@ -14,8 +16,20 @@ internal static class HtmlParser { return "\n"; } case "A": { - //TODO: implement parsing of links & mentions (automatically correct split domain mentions for the latter) - return null; + if (node is HtmlElement el) { + var href = el.GetAttribute("href"); + if (href == null) return $"{el.TextContent}"; + + if (el.ClassList.Contains("u-url") && el.ClassList.Contains("mention")) { + var mention = mentions.FirstOrDefault(p => p.Uri == href || p.Url == href); + if (mention != null) { + return $"@{mention.Username}@{mention.Host}"; + } + } + + return $"[{el.TextContent}]({href})"; + } + return node.TextContent; } case "H1": { return $"【{ParseChildren(node)}】\n"; @@ -74,7 +88,7 @@ internal static class HtmlParser { } } - private static string ParseChildren(INode node) { + private string ParseChildren(INode node) { return string.Join(null, node.ChildNodes.Select(ParseNode)); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/MfmParser.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/MfmParser.cs index ba709105..ec6829ec 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/MfmParser.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/MfmParser.cs @@ -1,10 +1,11 @@ +using System.Collections.Immutable; using System.Text.RegularExpressions; using Iceshrimp.Backend.Core.Helpers.LibMfm.Types; namespace Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; public static class MfmParser { - private static readonly List Parsers = [ + private static readonly ImmutableList Parsers = [ new PlainNodeParser(), new ItalicNodeParser(), new BoldNodeParser(), diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs new file mode 100644 index 00000000..fd891a20 --- /dev/null +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs @@ -0,0 +1,125 @@ +using System.Text; +using Iceshrimp.Backend.Core.Helpers.LibMfm.Types; + +namespace Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; + +public static class MfmSerializer { + public static string Serialize(IEnumerable nodes) { + var result = new StringBuilder(); + + foreach (var node in nodes) { + switch (node) { + case MfmCodeBlockNode mfmCodeBlockNode: { + result.Append($"```{mfmCodeBlockNode.Language ?? ""}\n"); + result.Append(mfmCodeBlockNode.Code); + result.Append("```"); + break; + } + case MfmMathBlockNode mfmMathBlockNode: { + result.Append(@"\["); + result.Append(mfmMathBlockNode.Formula); + result.Append(@"\]"); + break; + } + case MfmSearchNode mfmSearchNode: { + throw new NotImplementedException(); + break; + } + case MfmBoldNode mfmBoldNode: { + result.Append("**"); + result.Append(Serialize(node.Children)); + result.Append("**"); + break; + } + case MfmCenterNode mfmCenterNode: { + result.Append("
"); + result.Append(Serialize(node.Children)); + result.Append("
"); + break; + } + case MfmEmojiCodeNode mfmEmojiCodeNode: { + result.Append($":{mfmEmojiCodeNode.Name}:"); + break; + } + case MfmFnNode mfmFnNode: { + throw new NotImplementedException(); + break; + } + case MfmHashtagNode mfmHashtagNode: { + result.Append($"#{mfmHashtagNode.Hashtag}"); + break; + } + case MfmInlineCodeNode mfmInlineCodeNode: { + result.Append($"`{mfmInlineCodeNode.Code}`"); + break; + } + case MfmItalicNode mfmItalicNode: { + result.Append("~~"); + result.Append(Serialize(node.Children)); + result.Append("~~"); + break; + } + case MfmLinkNode mfmLinkNode: { + if (mfmLinkNode.Silent) result.Append('?'); + result.Append('['); + result.Append(Serialize(node.Children)); + result.Append(']'); + result.Append($"({mfmLinkNode.Url})"); + break; + } + case MfmMathInlineNode mfmMathInlineNode: { + result.Append(@"\("); + result.Append(mfmMathInlineNode.Formula); + result.Append(@"\)"); + break; + } + case MfmMentionNode mfmMentionNode: { + result.Append($"@{mfmMentionNode.Username}"); + if (mfmMentionNode.Host != null) + result.Append($"@{mfmMentionNode.Host}"); + break; + } + case MfmPlainNode: { + result.Append(node.Children.OfType().Select(p => p.Text)); + break; + } + case MfmSmallNode: { + result.Append(""); + result.Append(Serialize(node.Children)); + result.Append(""); + break; + } + case MfmStrikeNode: { + result.Append("~~"); + result.Append(Serialize(node.Children)); + result.Append("~~"); + break; + } + case MfmTextNode mfmTextNode: { + result.Append(mfmTextNode.Text); + break; + } + case MfmUnicodeEmojiNode mfmUnicodeEmojiNode: { + result.Append(mfmUnicodeEmojiNode.Emoji); + break; + } + case MfmUrlNode mfmUrlNode: { + if (mfmUrlNode.Brackets) + result.Append($"<{mfmUrlNode.Url}>"); + else + result.Append(mfmUrlNode.Url); + break; + } + case MfmQuoteNode mfmQuoteNode: { + throw new NotImplementedException(); + break; + } + default: { + throw new Exception("Unknown node type"); + } + } + } + + return result.ToString(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 3d6b2775..5dc4d12a 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -13,6 +13,12 @@ using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Core.Services; +using MentionQuad = + (List mentionedUserIds, + List mentions, + List remoteMentions, + Dictionary<(string usernameLower, string webDomain), string> splitDomainMapping); + [SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor", Justification = "We need IOptionsSnapshot for config hot reload")] public class NoteService( @@ -23,7 +29,9 @@ public class NoteService( ActivityFetcherService fetchSvc, ActivityDeliverService deliverSvc, NoteRenderer noteRenderer, - UserRenderer userRenderer + UserRenderer userRenderer, + MentionsResolver mentionsResolver, + MfmConverter mfmConverter ) { private readonly List _resolverHistory = []; private int _recursionLimit = 100; @@ -49,7 +57,7 @@ public class NoteService( UserHost = null, Visibility = visibility }; - + user.NotesCount++; await db.AddAsync(note); await db.SaveChangesAsync(); @@ -71,13 +79,13 @@ public class NoteService( } var user = await userResolver.ResolveAsync(actor.Id); - + // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataUsage (same reason as above) if (dbNote.User != user) { logger.LogDebug("Note '{id}' isn't owned by actor requesting its deletion, skipping", note.Id); return; } - + logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, user.Id); user.NotesCount--; @@ -117,14 +125,15 @@ public class NoteService( if (user.IsSuspended) throw GracefulException.Forbidden("User is suspended"); - //TODO: validate AP object type //TODO: resolve anything related to the note as well (attachments, emoji, etc) + var (mentionedUserIds, mentions, remoteMentions, splitDomainMapping) = await ResolveNoteMentionsAsync(note); + var dbNote = new Note { Id = IdHelpers.GenerateSlowflakeId(), Uri = note.Id, Url = note.Url?.Id, //FIXME: this doesn't seem to work yet - Text = note.MkContent ?? await MfmConverter.FromHtmlAsync(note.Content), + Text = note.MkContent ?? await mfmConverter.FromHtmlAsync(note.Content, mentions), UserId = user.Id, CreatedAt = note.PublishedAt?.ToUniversalTime() ?? throw GracefulException.UnprocessableEntity("Missing or invalid PublishedAt field"), @@ -137,6 +146,22 @@ public class NoteService( if (dbNote.Text is { Length: > 100000 }) throw GracefulException.UnprocessableEntity("Content cannot be longer than 100.000 characters"); + if (dbNote.Text is not null) { + dbNote.Mentions = mentionedUserIds; + dbNote.MentionedRemoteUsers = remoteMentions; + if (dbNote.Visibility == Note.NoteVisibility.Specified) { + var visibleUserIds = note.GetRecipients(actor).Concat(mentionedUserIds).ToList(); + if (dbNote.ReplyUserId != null) + visibleUserIds.Add(dbNote.ReplyUserId); + + dbNote.VisibleUserIds = visibleUserIds.Distinct().ToList(); + } + + dbNote.Text = + await mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, remoteMentions, + splitDomainMapping); + } + user.NotesCount++; await db.Notes.AddAsync(dbNote); await db.SaveChangesAsync(); @@ -144,6 +169,43 @@ public class NoteService( return dbNote; } + private async Task ResolveNoteMentionsAsync(ASNote note) { + var mentionTags = note.Tags?.OfType().Where(p => p.Href != null) ?? []; + var users = await mentionTags + .Select(async p => await userResolver.ResolveAsync(p.Href!.Id!)) + .AwaitAllNoConcurrencyAsync(); + + var userIds = users.Select(p => p.Id).ToList(); + + var remoteUsers = users.Where(p => p is { Host: not null, Uri: not null }) + .ToList(); + + var localUsers = users.Where(p => p.Host is null) + .ToList(); + + var splitDomainMapping = remoteUsers.Where(p => new Uri(p.Uri!).Host != p.Host) + .DistinctBy(p => p.Host) + .ToDictionary(p => (p.UsernameLower, new Uri(p.Uri!).Host), p => p.Host!); + + var localMentions = localUsers.Select(p => new Note.MentionedUser { + Host = config.Value.AccountDomain, + Username = p.Username, + Uri = $"https://{config.Value.WebDomain}/users/{p.Id}", + Url = $"https://{config.Value.WebDomain}/@{p.Username}" + }); + + var remoteMentions = remoteUsers.Select(p => new Note.MentionedUser { + Host = p.Host!, + Uri = p.Uri!, + Username = p.Username, + Url = p.UserProfile?.Url + }).ToList(); + + var mentions = remoteMentions.Concat(localMentions).ToList(); + + return (userIds, mentions, remoteMentions, splitDomainMapping); + } + public async Task ResolveNoteAsync(string uri) { //TODO: is this enough to prevent DoS attacks? if (_recursionLimit-- <= 0) @@ -159,7 +221,7 @@ public class NoteService( if (note != null) return note; //TODO: should we fall back to a regular user's keypair if fetching with instance actor fails & a local user is following the actor? - var fetchedNote = await fetchSvc.FetchNoteAsync(uri); + var fetchedNote = await fetchSvc.FetchNoteAsync(uri); if (fetchedNote?.AttributedTo is not [{ Id: not null } attrTo]) { logger.LogDebug("Invalid Note.AttributedTo, skipping"); return null; diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 63744629..fbe25a3d 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -24,9 +24,10 @@ public class UserService( ILogger logger, DatabaseContext db, ActivityFetcherService fetchSvc, - DriveService driveSvc + DriveService driveSvc, + MfmConverter mfmConverter ) { - private (string Username, string? Host) AcctToTuple(string acct) { + private static (string Username, string? Host) AcctToTuple(string acct) { if (!acct.StartsWith("acct:")) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query"); var split = acct[5..].Split('@'); @@ -43,7 +44,9 @@ public class UserService( throw GracefulException.NotFound("User not found"); } else { - return await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == query); + return await db.Users + .IncludeCommonProperties() + .FirstOrDefaultAsync(p => p.Uri != null && p.Uri.ToLower() == query.ToLowerInvariant()); } var tuple = AcctToTuple(query); @@ -51,7 +54,8 @@ public class UserService( tuple.Host = null; return await db.Users .IncludeCommonProperties() - .FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host); + .FirstOrDefaultAsync(p => p.UsernameLower == tuple.Username.ToLowerInvariant() && + p.Host == tuple.Host); } public async Task CreateUserAsync(string uri, string acct) { @@ -93,7 +97,7 @@ public class UserService( var profile = new UserProfile { User = user, - Description = actor.MkSummary ?? await MfmConverter.FromHtmlAsync(actor.Summary), + Description = actor.MkSummary ?? await mfmConverter.FromHtmlAsync(actor.Summary), //Birthday = TODO, //Location = TODO, //Fields = TODO, @@ -151,7 +155,7 @@ public class UserService( var processPendingDeletes = await ResolveAvatarAndBanner(user, actor); - user.UserProfile.Description = actor.MkSummary ?? await MfmConverter.FromHtmlAsync(actor.Summary); + user.UserProfile.Description = actor.MkSummary ?? await mfmConverter.FromHtmlAsync(actor.Summary); //user.UserProfile.Birthday = TODO; //user.UserProfile.Location = TODO; //user.UserProfile.Fields = TODO; diff --git a/Iceshrimp.Tests/Parsing/MfmTests.cs b/Iceshrimp.Tests/Parsing/MfmTests.cs index 0482e785..1386397a 100644 --- a/Iceshrimp.Tests/Parsing/MfmTests.cs +++ b/Iceshrimp.Tests/Parsing/MfmTests.cs @@ -1,5 +1,6 @@ using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; +using Microsoft.Extensions.DependencyInjection; namespace Iceshrimp.Tests.Parsing; @@ -24,8 +25,11 @@ public class MfmTests { return; async Task Benchmark() { + var provider = MockObjects.ServiceProvider; + var converter = provider.GetRequiredService(); + var pre = DateTime.Now; - await MfmConverter.ToHtmlAsync(Mfm); + await converter.ToHtmlAsync(Mfm, []); var post = DateTime.Now; var ms = (post - pre).TotalMilliseconds; Console.WriteLine($"Took {ms} ms");