From 75269176f0e92700b99a800035bd7ae45ad8c35b Mon Sep 17 00:00:00 2001 From: Kobe Albright Date: Sun, 19 Jan 2025 20:51:10 -0500 Subject: [PATCH] Channel migration system --- Valour/Database/Channel.cs | 2 +- .../20250120005514_IntVersion.Designer.cs | 2075 +++++++++++++++++ .../Migrations/20250120005514_IntVersion.cs | 34 + .../Migrations/ValourDbModelSnapshot.cs | 4 +- Valour/Server/Services/ChannelService.cs | 69 + Valour/Server/Workers/MigrationWorker.cs | 4 + 6 files changed, 2185 insertions(+), 3 deletions(-) create mode 100644 Valour/Database/Migrations/20250120005514_IntVersion.Designer.cs create mode 100644 Valour/Database/Migrations/20250120005514_IntVersion.cs diff --git a/Valour/Database/Channel.cs b/Valour/Database/Channel.cs index 365ac196..e586367e 100644 --- a/Valour/Database/Channel.cs +++ b/Valour/Database/Channel.cs @@ -80,7 +80,7 @@ public class Channel : ISharedChannel public bool IsDefault { get; set; } // Used for migrations - public byte Version { get; set; } + public int Version { get; set; } public static void SetupDbModel(ModelBuilder builder) { diff --git a/Valour/Database/Migrations/20250120005514_IntVersion.Designer.cs b/Valour/Database/Migrations/20250120005514_IntVersion.Designer.cs new file mode 100644 index 00000000..02d450d4 --- /dev/null +++ b/Valour/Database/Migrations/20250120005514_IntVersion.Designer.cs @@ -0,0 +1,2075 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Valour.Database.Context; + +#nullable disable + +namespace Valour.Database.Migrations +{ + [DbContext(typeof(ValourDb))] + [Migration("20250120005514_IntVersion")] + partial class IntVersion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Valour.Database.AuthToken", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("AppId") + .HasColumnType("text") + .HasColumnName("app_id"); + + b.Property("IssuedAddress") + .HasColumnType("text") + .HasColumnName("issued_address"); + + b.Property("Scope") + .HasColumnType("bigint") + .HasColumnName("scope"); + + b.Property("TimeCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_created"); + + b.Property("TimeExpires") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_expires"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Scope"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("auth_tokens", (string)null); + }); + + modelBuilder.Entity("Valour.Database.BlockedUserEmail", b => + { + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.HasKey("Email"); + + b.ToTable("blocked_user_emails"); + }); + + modelBuilder.Entity("Valour.Database.CdnBucketItem", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Category") + .HasColumnType("integer") + .HasColumnName("category"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.Property("MimeType") + .HasColumnType("text") + .HasColumnName("mime_type"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.ToTable("cdn_bucket_items"); + }); + + modelBuilder.Entity("Valour.Database.CdnProxyItem", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("MimeType") + .HasColumnType("text") + .HasColumnName("mime_type"); + + b.Property("Origin") + .HasColumnType("text") + .HasColumnName("origin"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id"); + + b.ToTable("cdn_proxies"); + }); + + modelBuilder.Entity("Valour.Database.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelType") + .HasColumnType("integer") + .HasColumnName("channel_type"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("InheritsPerms") + .HasColumnType("boolean") + .HasColumnName("inherits_perms"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastUpdateTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_update_time"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasColumnName("parent_id"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("RawPosition") + .HasColumnType("bigint") + .HasColumnName("position"); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("ParentId"); + + b.HasIndex("PlanetId"); + + b.HasIndex("RawPosition"); + + b.ToTable("channels", (string)null); + }); + + modelBuilder.Entity("Valour.Database.ChannelMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("bigint") + .HasColumnName("channel_id"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("UserId"); + + b.ToTable("channel_members"); + }); + + modelBuilder.Entity("Valour.Database.Credential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CredentialType") + .HasColumnType("text") + .HasColumnName("credential_type"); + + b.Property("Identifier") + .HasColumnType("text") + .HasColumnName("identifier"); + + b.Property("Salt") + .HasColumnType("bytea") + .HasColumnName("salt"); + + b.Property("Secret") + .HasColumnType("bytea") + .HasColumnName("secret"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("credentials"); + }); + + modelBuilder.Entity("Valour.Database.Economy.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DecimalPlaces") + .HasColumnType("integer") + .HasColumnName("decimal_places"); + + b.Property("Issued") + .HasColumnType("bigint") + .HasColumnName("issued"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("PluralName") + .HasColumnType("text") + .HasColumnName("plural_name"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("ShortCode") + .HasColumnType("text") + .HasColumnName("short_code"); + + b.Property("Symbol") + .HasColumnType("text") + .HasColumnName("symbol"); + + b.HasKey("Id"); + + b.ToTable("currencies"); + }); + + modelBuilder.Entity("Valour.Database.Economy.EcoAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountType") + .HasColumnType("integer") + .HasColumnName("account_type"); + + b.Property("BalanceValue") + .HasColumnType("numeric") + .HasColumnName("balance_value"); + + b.Property("CurrencyId") + .HasColumnType("bigint") + .HasColumnName("currency_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("PlanetMemberId") + .HasColumnType("bigint") + .HasColumnName("planet_member_id"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("PlanetMemberId"); + + b.HasIndex("UserId"); + + b.ToTable("eco_accounts"); + }); + + modelBuilder.Entity("Valour.Database.Economy.Transaction", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("AccountFromId") + .HasColumnType("bigint") + .HasColumnName("account_from_id"); + + b.Property("AccountToId") + .HasColumnType("bigint") + .HasColumnName("account_to_id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Fingerprint") + .HasColumnType("text") + .HasColumnName("fingerprint"); + + b.Property("ForcedBy") + .HasColumnType("bigint") + .HasColumnName("forced_by"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("TimeStamp") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_stamp"); + + b.Property("UserFromId") + .HasColumnType("bigint") + .HasColumnName("user_from_id"); + + b.Property("UserToId") + .HasColumnType("bigint") + .HasColumnName("user_to_id"); + + b.HasKey("Id"); + + b.HasIndex("AccountFromId"); + + b.HasIndex("AccountToId"); + + b.HasIndex("PlanetId"); + + b.HasIndex("UserFromId"); + + b.HasIndex("UserToId"); + + b.ToTable("transactions"); + }); + + modelBuilder.Entity("Valour.Database.EmailConfirmCode", b => + { + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Code"); + + b.HasIndex("UserId"); + + b.ToTable("email_confirm_codes"); + }); + + modelBuilder.Entity("Valour.Database.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachmentsData") + .HasColumnType("text") + .HasColumnName("attachments_data"); + + b.Property("AuthorMemberId") + .HasColumnType("bigint") + .HasColumnName("author_member_id"); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasColumnName("author_user_id"); + + b.Property("ChannelId") + .HasColumnType("bigint") + .HasColumnName("channel_id"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("EditedTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("edit_time"); + + b.Property("EmbedData") + .HasColumnType("text") + .HasColumnName("embed_data"); + + b.Property("MentionsData") + .HasColumnType("text") + .HasColumnName("mentions_data"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("ReplyToId") + .HasColumnType("bigint") + .HasColumnName("reply_to_id"); + + b.Property("TimeSent") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_sent"); + + b.HasKey("Id"); + + b.HasIndex("AuthorMemberId"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("PlanetId"); + + b.HasIndex("ReplyToId"); + + b.ToTable("messages", (string)null); + }); + + modelBuilder.Entity("Valour.Database.NodeStats", b => + { + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ActiveMemberCount") + .HasColumnType("integer") + .HasColumnName("active_member_count"); + + b.Property("ConnectionCount") + .HasColumnType("integer") + .HasColumnName("connection_count"); + + b.Property("ConnectionGroupCount") + .HasColumnType("integer") + .HasColumnName("connection_group_count"); + + b.Property("PlanetCount") + .HasColumnType("integer") + .HasColumnName("planet_count"); + + b.HasKey("Name"); + + b.ToTable("node_stats"); + }); + + modelBuilder.Entity("Valour.Database.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ChannelId") + .HasColumnType("bigint") + .HasColumnName("channel_id"); + + b.Property("ClickUrl") + .HasColumnType("text") + .HasColumnName("click_url"); + + b.Property("ImageUrl") + .HasColumnType("text") + .HasColumnName("image_url"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("Source") + .HasColumnType("integer") + .HasColumnName("source"); + + b.Property("SourceId") + .HasColumnType("bigint") + .HasColumnName("source_id"); + + b.Property("TimeRead") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_read"); + + b.Property("TimeSent") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_sent"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.ToTable("notifications"); + }); + + modelBuilder.Entity("Valour.Database.NotificationSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Auth") + .HasColumnType("text") + .HasColumnName("auth"); + + b.Property("Endpoint") + .HasColumnType("text") + .HasColumnName("endpoint"); + + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("notification_subscriptions"); + }); + + modelBuilder.Entity("Valour.Database.OauthApp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ImageUrl") + .HasColumnType("text") + .HasColumnName("image_url"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("bigint") + .HasColumnName("owner_id"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Secret") + .HasColumnType("text") + .HasColumnName("secret"); + + b.Property("Uses") + .HasColumnType("integer") + .HasColumnName("uses"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("oauth_apps", (string)null); + }); + + modelBuilder.Entity("Valour.Database.PasswordRecovery", b => + { + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Code"); + + b.HasIndex("UserId"); + + b.ToTable("password_recoveries"); + }); + + modelBuilder.Entity("Valour.Database.PermissionsNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("bigint") + .HasColumnName("code"); + + b.Property("Mask") + .HasColumnType("bigint") + .HasColumnName("mask"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.Property("TargetId") + .HasColumnType("bigint") + .HasColumnName("target_id"); + + b.Property("TargetType") + .HasColumnType("integer") + .HasColumnName("target_type"); + + b.HasKey("Id"); + + b.HasIndex("PlanetId"); + + b.HasIndex("RoleId"); + + b.HasIndex("TargetId"); + + b.ToTable("permissions_nodes"); + }); + + modelBuilder.Entity("Valour.Database.Planet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Discoverable") + .HasColumnType("boolean") + .HasColumnName("discoverable"); + + b.Property("HasAnimatedIcon") + .HasColumnType("boolean") + .HasColumnName("animated_icon"); + + b.Property("HasCustomIcon") + .HasColumnType("boolean") + .HasColumnName("custom_icon"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Nsfw") + .HasColumnType("boolean") + .HasColumnName("nsfw"); + + b.Property("OldIconUrl") + .HasColumnType("text") + .HasColumnName("icon_url"); + + b.Property("OwnerId") + .HasColumnType("bigint") + .HasColumnName("owner_id"); + + b.Property("Public") + .HasColumnType("boolean") + .HasColumnName("public"); + + b.HasKey("Id"); + + b.ToTable("planets"); + }); + + modelBuilder.Entity("Valour.Database.PlanetBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IssuerId") + .HasColumnType("bigint") + .HasColumnName("issuer_id"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("TargetId") + .HasColumnType("bigint") + .HasColumnName("target_id"); + + b.Property("TimeCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_created"); + + b.Property("TimeExpires") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_expires"); + + b.HasKey("Id"); + + b.HasIndex("PlanetId"); + + b.ToTable("planet_bans"); + }); + + modelBuilder.Entity("Valour.Database.PlanetInvite", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("IssuerId") + .HasColumnType("bigint") + .HasColumnName("issuer_id"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("TimeCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_created"); + + b.Property("TimeExpires") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_expires"); + + b.HasKey("Id"); + + b.HasIndex("IssuerId") + .IsUnique(); + + b.HasIndex("PlanetId"); + + b.HasIndex("TimeCreated", "TimeExpires"); + + b.ToTable("planet_invites", (string)null); + }); + + modelBuilder.Entity("Valour.Database.PlanetMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MemberAvatar") + .HasColumnType("text") + .HasColumnName("member_pfp"); + + b.Property("Nickname") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("nickname"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("RoleHashKey") + .HasColumnType("bigint") + .HasColumnName("role_hash_key"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("PlanetId"); + + b.HasIndex("RoleHashKey"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "PlanetId") + .IsUnique(); + + b.ToTable("planet_members", (string)null); + }); + + modelBuilder.Entity("Valour.Database.PlanetRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnyoneCanMention") + .HasColumnType("boolean") + .HasColumnName("anyone_can_mention"); + + b.Property("Bold") + .HasColumnType("boolean") + .HasColumnName("bold"); + + b.Property("CategoryPermissions") + .HasColumnType("bigint") + .HasColumnName("cat_perms"); + + b.Property("ChatPermissions") + .HasColumnType("bigint") + .HasColumnName("chat_perms"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("is_default"); + + b.Property("Italics") + .HasColumnType("boolean") + .HasColumnName("italics"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Permissions") + .HasColumnType("bigint") + .HasColumnName("permissions"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("Position") + .HasColumnType("bigint") + .HasColumnName("position"); + + b.Property("VoicePermissions") + .HasColumnType("bigint") + .HasColumnName("voice_perms"); + + b.HasKey("Id"); + + b.HasIndex("PlanetId"); + + b.ToTable("planet_roles"); + }); + + modelBuilder.Entity("Valour.Database.PlanetRoleMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasColumnName("member_id"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("MemberId"); + + b.HasIndex("PlanetId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("planet_role_members"); + }); + + modelBuilder.Entity("Valour.Database.Referral", b => + { + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("ReferrerId") + .HasColumnType("bigint") + .HasColumnName("referrer_id"); + + b.Property("Reward") + .HasColumnType("numeric") + .HasColumnName("reward"); + + b.HasKey("UserId"); + + b.HasIndex("ReferrerId"); + + b.ToTable("referrals"); + }); + + modelBuilder.Entity("Valour.Database.Report", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint") + .HasColumnName("channel_id"); + + b.Property("LongReason") + .HasColumnType("text") + .HasColumnName("long_reason"); + + b.Property("MessageId") + .HasColumnType("bigint") + .HasColumnName("message_id"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.Property("ReasonCode") + .HasColumnType("bigint") + .HasColumnName("reason_code"); + + b.Property("ReportingUserId") + .HasColumnType("bigint") + .HasColumnName("reporting_user_id"); + + b.Property("Reviewed") + .HasColumnType("boolean") + .HasColumnName("reviewed"); + + b.Property("TimeCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_created"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("MessageId"); + + b.HasIndex("PlanetId"); + + b.HasIndex("ReportingUserId"); + + b.ToTable("reports", (string)null); + }); + + modelBuilder.Entity("Valour.Database.StatObject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryCount") + .HasColumnType("integer") + .HasColumnName("category_count"); + + b.Property("ChannelCount") + .HasColumnType("integer") + .HasColumnName("channel_count"); + + b.Property("MessageDayCount") + .HasColumnType("integer") + .HasColumnName("message_day_count"); + + b.Property("MessagesSent") + .HasColumnType("integer") + .HasColumnName("messages_sent"); + + b.Property("PlanetCount") + .HasColumnType("integer") + .HasColumnName("planet_count"); + + b.Property("PlanetMemberCount") + .HasColumnType("integer") + .HasColumnName("planet_member_count"); + + b.Property("TimeCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_created"); + + b.Property("UserCount") + .HasColumnType("integer") + .HasColumnName("user_count"); + + b.HasKey("Id"); + + b.ToTable("stat_objects"); + }); + + modelBuilder.Entity("Valour.Database.TenorFavorite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("TenorId") + .HasColumnType("text") + .HasColumnName("tenor_id"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.ToTable("tenor_favorites"); + }); + + modelBuilder.Entity("Valour.Database.Themes.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint") + .HasColumnName("author_id"); + + b.Property("CustomCss") + .HasColumnType("text") + .HasColumnName("custom_css"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FontAltColor") + .HasColumnType("text") + .HasColumnName("font_alt_color"); + + b.Property("FontColor") + .HasColumnType("text") + .HasColumnName("font_color"); + + b.Property("HasAnimatedBanner") + .HasColumnType("boolean") + .HasColumnName("animated_banner"); + + b.Property("HasCustomBanner") + .HasColumnType("boolean") + .HasColumnName("custom_banner"); + + b.Property("LinkColor") + .HasColumnType("text") + .HasColumnName("link_color"); + + b.Property("MainColor1") + .HasColumnType("text") + .HasColumnName("main_color_1"); + + b.Property("MainColor2") + .HasColumnType("text") + .HasColumnName("main_color_2"); + + b.Property("MainColor3") + .HasColumnType("text") + .HasColumnName("main_color_3"); + + b.Property("MainColor4") + .HasColumnType("text") + .HasColumnName("main_color_4"); + + b.Property("MainColor5") + .HasColumnType("text") + .HasColumnName("main_color_5"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PastelCyan") + .HasColumnType("text") + .HasColumnName("pastel_cyan"); + + b.Property("PastelCyanPurple") + .HasColumnType("text") + .HasColumnName("pastel_cyan_purple"); + + b.Property("PastelPurple") + .HasColumnType("text") + .HasColumnName("pastel_purple"); + + b.Property("PastelRed") + .HasColumnType("text") + .HasColumnName("pastel_red"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.Property("TintColor") + .HasColumnType("text") + .HasColumnName("tint_color"); + + b.Property("VibrantBlue") + .HasColumnType("text") + .HasColumnName("vibrant_blue"); + + b.Property("VibrantCyan") + .HasColumnType("text") + .HasColumnName("vibrant_cyan"); + + b.Property("VibrantPurple") + .HasColumnType("text") + .HasColumnName("vibrant_purple"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("themes"); + }); + + modelBuilder.Entity("Valour.Database.Themes.ThemeVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Sentiment") + .HasColumnType("boolean") + .HasColumnName("sentiment"); + + b.Property("ThemeId") + .HasColumnType("bigint") + .HasColumnName("theme_id"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("ThemeId"); + + b.HasIndex("UserId"); + + b.ToTable("theme_votes"); + }); + + modelBuilder.Entity("Valour.Database.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bot") + .HasColumnType("boolean") + .HasColumnName("bot"); + + b.Property("Compliance") + .HasColumnType("boolean") + .HasColumnName("compliance"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("HasAnimatedAvatar") + .HasColumnType("boolean") + .HasColumnName("animated_avatar"); + + b.Property("HasCustomAvatar") + .HasColumnType("boolean") + .HasColumnName("custom_avatar"); + + b.Property("IsMobile") + .HasColumnType("boolean") + .HasColumnName("is_mobile"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("NameChangeTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("name_change_time"); + + b.Property("OldAvatarUrl") + .HasColumnType("text") + .HasColumnName("pfp_url"); + + b.Property("PriorName") + .HasColumnType("text") + .HasColumnName("prior_name"); + + b.Property("Status") + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("SubscriptionType") + .HasColumnType("text") + .HasColumnName("subscription_type"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("TimeJoined") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_joined"); + + b.Property("TimeLastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("time_last_active"); + + b.Property("UserStateCode") + .HasColumnType("integer") + .HasColumnName("user_state_code"); + + b.Property("ValourStaff") + .HasColumnType("boolean") + .HasColumnName("valour_staff"); + + b.HasKey("Id"); + + b.HasIndex("TimeLastActive"); + + b.HasIndex("Tag", "Name") + .IsUnique(); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Valour.Database.UserChannelState", b => + { + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.Property("ChannelId") + .HasColumnType("bigint") + .HasColumnName("channel_id"); + + b.Property("LastViewedTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_viewed_time"); + + b.Property("PlanetId") + .HasColumnType("bigint") + .HasColumnName("planet_id"); + + b.HasKey("UserId", "ChannelId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("PlanetId"); + + b.HasIndex("UserId"); + + b.ToTable("user_channel_states", (string)null); + }); + + modelBuilder.Entity("Valour.Database.UserFriend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendId") + .HasColumnType("bigint") + .HasColumnName("friend_id"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FriendId"); + + b.HasIndex("UserId"); + + b.ToTable("user_friends"); + }); + + modelBuilder.Entity("Valour.Database.UserPrivateInfo", b => + { + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("birth_date"); + + b.Property("JoinInviteCode") + .HasColumnType("text") + .HasColumnName("join_invite_code"); + + b.Property("JoinSource") + .HasColumnType("text") + .HasColumnName("join_source"); + + b.Property("Locality") + .HasColumnType("integer") + .HasColumnName("locality"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.Property("Verified") + .HasColumnType("boolean") + .HasColumnName("verified"); + + b.HasKey("Email"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_emails"); + }); + + modelBuilder.Entity("Valour.Database.UserProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnimatedBorder") + .HasColumnType("boolean") + .HasColumnName("anim_border"); + + b.Property("BackgroundImage") + .HasColumnType("text") + .HasColumnName("bg_image"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("BorderColor") + .HasColumnType("text") + .HasColumnName("border_color"); + + b.Property("GlowColor") + .HasColumnType("text") + .HasColumnName("glow_color"); + + b.Property("Headline") + .HasColumnType("text") + .HasColumnName("headline"); + + b.Property("PrimaryColor") + .HasColumnType("text") + .HasColumnName("primary_color"); + + b.Property("SecondaryColor") + .HasColumnType("text") + .HasColumnName("secondary_color"); + + b.Property("TertiaryColor") + .HasColumnType("text") + .HasColumnName("tertiary_color"); + + b.Property("TextColor") + .HasColumnType("text") + .HasColumnName("text_color"); + + b.HasKey("Id"); + + b.ToTable("user_profiles"); + }); + + modelBuilder.Entity("Valour.Database.UserSubscription", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Cancelled") + .HasColumnType("boolean") + .HasColumnName("cancelled"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("LastCharged") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_charged"); + + b.Property("Renewals") + .HasColumnType("integer") + .HasColumnName("renewals"); + + b.Property("Type") + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_subscriptions", (string)null); + }); + + modelBuilder.Entity("Valour.Database.AuthToken", b => + { + b.HasOne("Valour.Database.User", "User") + .WithMany("AuthTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Channel", b => + { + b.HasOne("Valour.Database.Channel", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("Channels") + .HasForeignKey("PlanetId"); + + b.Navigation("Parent"); + + b.Navigation("Planet"); + }); + + modelBuilder.Entity("Valour.Database.ChannelMember", b => + { + b.HasOne("Valour.Database.Channel", "Channel") + .WithMany("Members") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Credential", b => + { + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Economy.EcoAccount", b => + { + b.HasOne("Valour.Database.PlanetMember", "PlanetMember") + .WithMany() + .HasForeignKey("PlanetMemberId"); + + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlanetMember"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Economy.Transaction", b => + { + b.HasOne("Valour.Database.Economy.EcoAccount", "AccountFrom") + .WithMany() + .HasForeignKey("AccountFromId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Economy.EcoAccount", "AccountTo") + .WithMany() + .HasForeignKey("AccountToId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany() + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "UserFrom") + .WithMany() + .HasForeignKey("UserFromId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "UserTo") + .WithMany() + .HasForeignKey("UserToId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AccountFrom"); + + b.Navigation("AccountTo"); + + b.Navigation("Planet"); + + b.Navigation("UserFrom"); + + b.Navigation("UserTo"); + }); + + modelBuilder.Entity("Valour.Database.EmailConfirmCode", b => + { + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Message", b => + { + b.HasOne("Valour.Database.PlanetMember", "AuthorMember") + .WithMany("Messages") + .HasForeignKey("AuthorMemberId"); + + b.HasOne("Valour.Database.User", "AuthorUser") + .WithMany("Messages") + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Channel", "Channel") + .WithMany("Messages") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("Messages") + .HasForeignKey("PlanetId"); + + b.HasOne("Valour.Database.Message", "ReplyToMessage") + .WithMany("Replies") + .HasForeignKey("ReplyToId"); + + b.Navigation("AuthorMember"); + + b.Navigation("AuthorUser"); + + b.Navigation("Channel"); + + b.Navigation("Planet"); + + b.Navigation("ReplyToMessage"); + }); + + modelBuilder.Entity("Valour.Database.NotificationSubscription", b => + { + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.OauthApp", b => + { + b.HasOne("Valour.Database.User", "Owner") + .WithMany("OwnedApps") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Valour.Database.PasswordRecovery", b => + { + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.PermissionsNode", b => + { + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany() + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.PlanetRole", "Role") + .WithMany("PermissionNodes") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Channel", "Target") + .WithMany("Permissions") + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Planet"); + + b.Navigation("Role"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Valour.Database.PlanetBan", b => + { + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany() + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Planet"); + }); + + modelBuilder.Entity("Valour.Database.PlanetInvite", b => + { + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("Invites") + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Planet"); + }); + + modelBuilder.Entity("Valour.Database.PlanetMember", b => + { + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("Members") + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "User") + .WithMany("Membership") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Planet"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.PlanetRole", b => + { + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("Roles") + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Planet"); + }); + + modelBuilder.Entity("Valour.Database.PlanetRoleMember", b => + { + b.HasOne("Valour.Database.PlanetMember", "Member") + .WithMany("RoleMembership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("RoleMembers") + .HasForeignKey("PlanetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.PlanetRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Member"); + + b.Navigation("Planet"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Referral", b => + { + b.HasOne("Valour.Database.User", "Referrer") + .WithMany() + .HasForeignKey("ReferrerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Referrer"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Themes.Theme", b => + { + b.HasOne("Valour.Database.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Valour.Database.Themes.ThemeVote", b => + { + b.HasOne("Valour.Database.Themes.Theme", "Theme") + .WithMany("ThemeVotes") + .HasForeignKey("ThemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Theme"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.UserChannelState", b => + { + b.HasOne("Valour.Database.Channel", "Channel") + .WithMany("UserChannelStates") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.Planet", "Planet") + .WithMany("UserChannelStates") + .HasForeignKey("PlanetId"); + + b.HasOne("Valour.Database.User", "User") + .WithMany("ChannelStates") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("Planet"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.UserFriend", b => + { + b.HasOne("Valour.Database.User", "Friend") + .WithMany() + .HasForeignKey("FriendId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Valour.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Friend"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.UserPrivateInfo", b => + { + b.HasOne("Valour.Database.User", "User") + .WithOne("PrivateInfo") + .HasForeignKey("Valour.Database.UserPrivateInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.UserSubscription", b => + { + b.HasOne("Valour.Database.User", "User") + .WithMany("Subscriptions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Valour.Database.Channel", b => + { + b.Navigation("Children"); + + b.Navigation("Members"); + + b.Navigation("Messages"); + + b.Navigation("Permissions"); + + b.Navigation("UserChannelStates"); + }); + + modelBuilder.Entity("Valour.Database.Message", b => + { + b.Navigation("Replies"); + }); + + modelBuilder.Entity("Valour.Database.Planet", b => + { + b.Navigation("Channels"); + + b.Navigation("Invites"); + + b.Navigation("Members"); + + b.Navigation("Messages"); + + b.Navigation("RoleMembers"); + + b.Navigation("Roles"); + + b.Navigation("UserChannelStates"); + }); + + modelBuilder.Entity("Valour.Database.PlanetMember", b => + { + b.Navigation("Messages"); + + b.Navigation("RoleMembership"); + }); + + modelBuilder.Entity("Valour.Database.PlanetRole", b => + { + b.Navigation("PermissionNodes"); + }); + + modelBuilder.Entity("Valour.Database.Themes.Theme", b => + { + b.Navigation("ThemeVotes"); + }); + + modelBuilder.Entity("Valour.Database.User", b => + { + b.Navigation("AuthTokens"); + + b.Navigation("ChannelStates"); + + b.Navigation("Membership"); + + b.Navigation("Messages"); + + b.Navigation("OwnedApps"); + + b.Navigation("PrivateInfo"); + + b.Navigation("Subscriptions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Valour/Database/Migrations/20250120005514_IntVersion.cs b/Valour/Database/Migrations/20250120005514_IntVersion.cs new file mode 100644 index 00000000..3424ec48 --- /dev/null +++ b/Valour/Database/Migrations/20250120005514_IntVersion.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Valour.Database.Migrations +{ + /// + public partial class IntVersion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "version", + table: "channels", + type: "integer", + nullable: false, + oldClrType: typeof(byte), + oldType: "smallint"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "version", + table: "channels", + type: "smallint", + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + } + } +} diff --git a/Valour/Database/Migrations/ValourDbModelSnapshot.cs b/Valour/Database/Migrations/ValourDbModelSnapshot.cs index 1b026022..8adf373f 100644 --- a/Valour/Database/Migrations/ValourDbModelSnapshot.cs +++ b/Valour/Database/Migrations/ValourDbModelSnapshot.cs @@ -183,8 +183,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bigint") .HasColumnName("position"); - b.Property("Version") - .HasColumnType("smallint") + b.Property("Version") + .HasColumnType("integer") .HasColumnName("version"); b.HasKey("Id"); diff --git a/Valour/Server/Services/ChannelService.cs b/Valour/Server/Services/ChannelService.cs index 23988e85..4012f02f 100644 --- a/Valour/Server/Services/ChannelService.cs +++ b/Valour/Server/Services/ChannelService.cs @@ -731,4 +731,73 @@ public async Task> TryGetNextChannelPosition(long? planetId, Ch return TaskResult.FromData(newPosition); } + + private static int _migratedChannels = 0; + + public async Task MigrateChannels() + { + // From V0 -> V2, we convert the position to the new format + + // Non-planet channels just get updated to version 2 + await _db.Channels.Where(x => x.PlanetId == null) + .ExecuteUpdateAsync(u => u.SetProperty(c => c.Version, 2)); + + var rootChannels = await _db.Channels + .Where(x => x.Version < 2 + && x.PlanetId != null + && x.ParentId == null) + .ToListAsync(); + + // Build set of planets + var planets = new HashSet(); + foreach (var root in rootChannels) + { + planets.Add(root.PlanetId!.Value); + } + + foreach (var planetId in planets) + { + var rootChannelsForPlanet = rootChannels.Where(x => x.PlanetId == planetId).ToList(); + + // Sort the root channels by position + rootChannelsForPlanet.Sort((a, b) => a.RawPosition.CompareTo(b.RawPosition)); + + var ri = 0; + foreach (var rootChannel in rootChannelsForPlanet) + { + var rootPos = ChannelPosition.AppendRelativePosition(0, (uint)ri, 0); + ri++; + + await MigrateChannel(rootChannel, rootPos); + + await _db.SaveChangesAsync(); + + _logger.LogInformation("Migrated channel tree, total {ChannelCount}", _migratedChannels); + } + } + } + + public async Task MigrateChannel(Valour.Database.Channel channel, uint newPosition) + { + channel.RawPosition = newPosition; + channel.Version = 2; + + _db.Channels.Update(channel); + + _migratedChannels++; + + // Migrate children + var children = await _db.Channels + .Where(x => x.ParentId == channel.Id) + .OrderBy(x => x.RawPosition) + .ToListAsync(); + + var ci = 0; + foreach (var child in children) + { + var childPos = ChannelPosition.AppendRelativePosition(newPosition, (uint)ci); + ci++; + await MigrateChannel(child, childPos); + } + } } \ No newline at end of file diff --git a/Valour/Server/Workers/MigrationWorker.cs b/Valour/Server/Workers/MigrationWorker.cs index 35e6dd63..f28d4f90 100644 --- a/Valour/Server/Workers/MigrationWorker.cs +++ b/Valour/Server/Workers/MigrationWorker.cs @@ -23,6 +23,7 @@ public async Task StartAsync(CancellationToken cancellationToken) using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var permService = scope.ServiceProvider.GetRequiredService(); + var channelService = scope.ServiceProvider.GetRequiredService(); // Perform startup tasks var startupService = scope.ServiceProvider.GetRequiredService(); @@ -31,6 +32,9 @@ public async Task StartAsync(CancellationToken cancellationToken) // Generate role hash keys for all members await permService.BulkUpdateMemberRoleHashesAsync(); _logger.LogInformation("Migration Worker has finished"); + + // Migrate channels + await channelService.MigrateChannels(); } public Task StopAsync(CancellationToken stoppingToken)