diff --git a/.gitignore b/.gitignore index 9037fa0..b1e845f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ docker/proxy-config.json docker/frontend.env Foxnouns.DataMigrator/apps.json +migration-tools/avatar-proxy/config.json +migration-tools/avatar-migrator/.env out/ build/ diff --git a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs new file mode 100644 index 0000000..cb9377d --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs @@ -0,0 +1,923 @@ +// +using System.Collections.Generic; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250410192220_AddAvatarMigrations")] + partial class AddAvatarMigrations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.PrimitiveCollection("RedirectUris") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("redirect_uris"); + + b.PrimitiveCollection("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.ToTable("applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.PrimitiveCollection("ClearedFields") + .HasColumnType("text[]") + .HasColumnName("cleared_fields"); + + b.Property("ModeratorId") + .HasColumnType("bigint") + .HasColumnName("moderator_id"); + + b.Property("ModeratorUsername") + .IsRequired() + .HasColumnType("text") + .HasColumnName("moderator_username"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("ReportId") + .HasColumnType("bigint") + .HasColumnName("report_id"); + + b.Property("TargetMemberId") + .HasColumnType("bigint") + .HasColumnName("target_member_id"); + + b.Property("TargetMemberName") + .HasColumnType("text") + .HasColumnName("target_member_name"); + + b.Property("TargetUserId") + .HasColumnType("bigint") + .HasColumnName("target_user_id"); + + b.Property("TargetUsername") + .HasColumnType("text") + .HasColumnName("target_username"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_audit_log"); + + b.HasIndex("ReportId") + .IsUnique() + .HasDatabaseName("ix_audit_log_report_id"); + + b.ToTable("audit_log", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_methods"); + + b.HasIndex("FediverseApplicationId") + .HasDatabaseName("ix_auth_methods_fediverse_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_methods_user_id"); + + b.HasIndex("AuthType", "RemoteId") + .IsUnique() + .HasDatabaseName("ix_auth_methods_auth_type_remote_id") + .HasFilter("fediverse_application_id IS NULL"); + + b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId") + .IsUnique() + .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id") + .HasFilter("fediverse_application_id IS NOT NULL"); + + b.ToTable("auth_methods", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Filename") + .IsRequired() + .HasColumnType("text") + .HasColumnName("filename"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_data_exports"); + + b.HasIndex("Filename") + .IsUnique() + .HasDatabaseName("ix_data_exports_filename"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_data_exports_user_id"); + + b.ToTable("data_exports", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("ForceRefresh") + .HasColumnType("boolean") + .HasColumnName("force_refresh"); + + b.Property("InstanceType") + .HasColumnType("integer") + .HasColumnName("instance_type"); + + b.HasKey("Id") + .HasName("pk_fediverse_applications"); + + b.ToTable("fediverse_applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("AvatarMigrated") + .HasColumnType("boolean") + .HasColumnName("avatar_migrated"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("LegacyId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("legacy_id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Names") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("names"); + + b.Property>("Pronouns") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("pronouns"); + + b.Property("Sid") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("sid") + .HasDefaultValueSql("find_free_member_sid()"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_members_legacy_id"); + + b.HasIndex("Sid") + .IsUnique() + .HasDatabaseName("ix_members_sid"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasDatabaseName("ix_members_user_id_name"); + + b.ToTable("members", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasColumnName("member_id"); + + b.Property("PrideFlagId") + .HasColumnType("bigint") + .HasColumnName("pride_flag_id"); + + b.HasKey("Id") + .HasName("pk_member_flags"); + + b.HasIndex("MemberId") + .HasDatabaseName("ix_member_flags_member_id"); + + b.HasIndex("PrideFlagId") + .HasDatabaseName("ix_member_flags_pride_flag_id"); + + b.ToTable("member_flags", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("bigint") + .HasColumnName("author_id"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.HasKey("Id") + .HasName("pk_notices"); + + b.HasIndex("AuthorId") + .HasDatabaseName("ix_notices_author_id"); + + b.ToTable("notices", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AcknowledgedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("acknowledged_at"); + + b.Property("LocalizationKey") + .HasColumnType("text") + .HasColumnName("localization_key"); + + b.Property>("LocalizationParams") + .IsRequired() + .HasColumnType("hstore") + .HasColumnName("localization_params"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("TargetId") + .HasColumnType("bigint") + .HasColumnName("target_id"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_notifications"); + + b.HasIndex("TargetId") + .HasDatabaseName("ix_notifications_target_id"); + + b.ToTable("notifications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.Property("LegacyId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("legacy_id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_pride_flags"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_pride_flags_legacy_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_pride_flags_user_id"); + + b.ToTable("pride_flags", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("Reason") + .HasColumnType("integer") + .HasColumnName("reason"); + + b.Property("ReporterId") + .HasColumnType("bigint") + .HasColumnName("reporter_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TargetMemberId") + .HasColumnType("bigint") + .HasColumnName("target_member_id"); + + b.Property("TargetSnapshot") + .HasColumnType("text") + .HasColumnName("target_snapshot"); + + b.Property("TargetType") + .HasColumnType("integer") + .HasColumnName("target_type"); + + b.Property("TargetUserId") + .HasColumnType("bigint") + .HasColumnName("target_user_id"); + + b.HasKey("Id") + .HasName("pk_reports"); + + b.HasIndex("ReporterId") + .HasDatabaseName("ix_reports_reporter_id"); + + b.HasIndex("TargetMemberId") + .HasDatabaseName("ix_reports_target_member_id"); + + b.HasIndex("TargetUserId") + .HasDatabaseName("ix_reports_target_user_id"); + + b.ToTable("reports", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("bigint") + .HasColumnName("application_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.PrimitiveCollection("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("AvatarMigrated") + .HasColumnType("boolean") + .HasColumnName("avatar_migrated"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property>("CustomPreferences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("custom_preferences"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasColumnName("deleted_by"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_active"); + + b.Property("LastSidReroll") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sid_reroll"); + + b.Property("LegacyId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("legacy_id") + .HasDefaultValueSql("gen_random_uuid()"); + + b.PrimitiveCollection("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("ListHidden") + .HasColumnType("boolean") + .HasColumnName("list_hidden"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property>("Names") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("names"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property>("Pronouns") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("pronouns"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Settings") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("settings"); + + b.Property("Sid") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("sid") + .HasDefaultValueSql("find_free_user_sid()"); + + b.Property("Timezone") + .HasColumnType("text") + .HasColumnName("timezone"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("LegacyId") + .IsUnique() + .HasDatabaseName("ix_users_legacy_id"); + + b.HasIndex("Sid") + .IsUnique() + .HasDatabaseName("ix_users_sid"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrideFlagId") + .HasColumnType("bigint") + .HasColumnName("pride_flag_id"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_flags"); + + b.HasIndex("PrideFlagId") + .HasDatabaseName("ix_user_flags_pride_flag_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_flags_user_id"); + + b.ToTable("user_flags", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report") + .WithOne("AuditLogEntry") + .HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_audit_log_reports_report_id"); + + b.Navigation("Report"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") + .WithMany() + .HasForeignKey("FediverseApplicationId") + .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("AuthMethods") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_methods_users_user_id"); + + b.Navigation("FediverseApplication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("DataExports") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_data_exports_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("Members") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_members_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Member", null) + .WithMany("ProfileFlags") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_member_flags_members_member_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") + .WithMany() + .HasForeignKey("PrideFlagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_member_flags_pride_flags_pride_flag_id"); + + b.Navigation("PrideFlag"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notices_users_author_id"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "Target") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifications_users_target_id"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", null) + .WithMany("Flags") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pride_flags_users_user_id"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter") + .WithMany() + .HasForeignKey("ReporterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reports_users_reporter_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember") + .WithMany() + .HasForeignKey("TargetMemberId") + .HasConstraintName("fk_reports_members_target_member_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reports_users_target_user_id"); + + b.Navigation("Reporter"); + + b.Navigation("TargetMember"); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + + b.Navigation("Application"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") + .WithMany() + .HasForeignKey("PrideFlagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_flags_pride_flags_pride_flag_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", null) + .WithMany("ProfileFlags") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_flags_users_user_id"); + + b.Navigation("PrideFlag"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Navigation("ProfileFlags"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => + { + b.Navigation("AuditLogEntry"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Navigation("AuthMethods"); + + b.Navigation("DataExports"); + + b.Navigation("Flags"); + + b.Navigation("Members"); + + b.Navigation("ProfileFlags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs new file mode 100644 index 0000000..ca88605 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + public partial class AddAvatarMigrations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "avatar_migrated", + table: "users", + type: "boolean", + nullable: false, + defaultValue: false + ); + + migrationBuilder.AddColumn( + name: "avatar_migrated", + table: "members", + type: "boolean", + nullable: false, + defaultValue: false + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn(name: "avatar_migrated", table: "users"); + + migrationBuilder.DropColumn(name: "avatar_migrated", table: "members"); + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 70b035d..92db9f9 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -241,6 +241,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("avatar"); + b.Property("AvatarMigrated") + .HasColumnType("boolean") + .HasColumnName("avatar_migrated"); + b.Property("Bio") .HasColumnType("text") .HasColumnName("bio"); @@ -565,6 +569,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("avatar"); + b.Property("AvatarMigrated") + .HasColumnType("boolean") + .HasColumnName("avatar_migrated"); + b.Property("Bio") .HasColumnType("text") .HasColumnName("bio"); diff --git a/Foxnouns.Backend/Database/Models/Member.cs b/Foxnouns.Backend/Database/Models/Member.cs index 81a01d8..85b39f3 100644 --- a/Foxnouns.Backend/Database/Models/Member.cs +++ b/Foxnouns.Backend/Database/Models/Member.cs @@ -29,6 +29,9 @@ public class Member : BaseModel public List Pronouns { get; set; } = []; public List Fields { get; set; } = []; + // Only used by avatar-proxy and avatar-migration. + public bool AvatarMigrated { get; set; } = true; + public List ProfileFlags { get; set; } = []; public Snowflake UserId { get; init; } diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index 0e6eb43..fe97b6c 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -57,6 +57,9 @@ public class User : BaseModel public Instant? DeletedAt { get; set; } public Snowflake? DeletedBy { get; set; } + // Only used by avatar-proxy and avatar-migration. + public bool AvatarMigrated { get; set; } = true; + [NotMapped] public bool? SelfDelete => Deleted ? DeletedBy != null : null; diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs index 907dfc4..c1d2df4 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs @@ -67,6 +67,7 @@ public class MemberAvatarUpdateJob( await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); member.Avatar = hash; + member.AvatarMigrated = true; await db.SaveChangesAsync(); if (prevHash != null && prevHash != hash) diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs index 1ab446c..cf7bed0 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs @@ -68,6 +68,7 @@ public class UserAvatarUpdateJob( await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); user.Avatar = hash; + user.AvatarMigrated = true; await db.SaveChangesAsync(); if (prevHash != null && prevHash != hash) diff --git a/migration-tools/avatar-migrator/index.js b/migration-tools/avatar-migrator/index.js new file mode 100644 index 0000000..1415fa8 --- /dev/null +++ b/migration-tools/avatar-migrator/index.js @@ -0,0 +1,82 @@ +// TODO: i'm not even sure if this code works. it's not easy to test either. woops + +import postgres from "postgres"; +import { config } from "dotenv"; +import { Client } from "minio"; +import { Logger } from "tslog"; +import axios from "axios"; +config(); +const log = new Logger(); + +const env = (key) => { + const value = process.env[key]; + if (value) return value; + throw `No env variable with key $${key} found`; +}; + +const oldBaseUrl = env("OLD_BASE_URL"); +const bucket = env("MINIO_BUCKET"); + +const sql = postgres(env("DATABASE_URL")); +const minio = new Client({ + endPoint: env("MINIO_ENDPOINT"), + useSSL: true, + accessKey: env("MINIO_ACCESS_KEY"), + secretKey: env("MINIO_SECRET_KEY"), +}); + +const users = + await sql`select id::text, username, legacy_id, avatar from users where avatar is not null order by id asc`; +log.info("have to migrate %d users", users.length); + +const migrate = async (user) => { + log.debug( + "copying /users/%s/%s.webp to /users/%s/avatars/%s.webp", + user.legacy_id, + user.avatar, + user.id, + user.avatar + ); + + try { + const file = await axios.get( + `${oldBaseUrl}/users/${user.legacy_id}/${user.avatar}.webp`, + { responseType: "stream" } + ); + await minio.putObject( + bucket, + `users/${user.id}/avatars/${user.avatar}.webp`, + file.data, + file.headers["Content-Length"] + ); + + log.info("copied avatar for user %s", user.id); + + await sql`update users set avatar_migrated = true where id = ${user.id}::bigint`; + } catch (e) { + if ("status" in e && e.status === 404) { + log.warn( + "avatar for user %s/%s is not found. marking it as migrated.", + user.id, + user.username + ); + await sql`update users set avatar_migrated = true where id = ${user.id}::bigint`; + return; + } + + log.error( + "could not migrate avatar for user %s/%s:", + user.id, + user.username, + e + ); + } +}; + +for (let index = 0; index < users.length; index++) { + const user = users[index]; + await migrate(user); +} + +log.info("all users migrated!"); +process.exit(); diff --git a/migration-tools/avatar-migrator/package-lock.json b/migration-tools/avatar-migrator/package-lock.json new file mode 100644 index 0000000..6aa1e3d --- /dev/null +++ b/migration-tools/avatar-migrator/package-lock.json @@ -0,0 +1,866 @@ +{ + "name": "avatar-migrator", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.8.4", + "dotenv": "^16.5.0", + "minio": "^8.0.5", + "postgres": "^3.4.5", + "tslog": "^4.9.3" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minio": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz", + "integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^1.0.0", + "eventemitter3": "^5.0.1", + "fast-xml-parser": "^4.4.1", + "ipaddr.js": "^2.0.1", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "stream-json": "^1.8.0", + "through2": "^4.0.2", + "web-encoding": "^1.1.5", + "xml2js": "^0.5.0 || ^0.6.2" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz", + "integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tslog": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", + "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/fullstack-build/tslog?sponsor=1" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "license": "MIT", + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/migration-tools/avatar-migrator/package.json b/migration-tools/avatar-migrator/package.json new file mode 100644 index 0000000..e9d3463 --- /dev/null +++ b/migration-tools/avatar-migrator/package.json @@ -0,0 +1,10 @@ +{ + "type": "module", + "dependencies": { + "axios": "^1.8.4", + "dotenv": "^16.5.0", + "minio": "^8.0.5", + "postgres": "^3.4.5", + "tslog": "^4.9.3" + } +} diff --git a/migration-tools/avatar-proxy/config.example.json b/migration-tools/avatar-proxy/config.example.json new file mode 100644 index 0000000..d04cb71 --- /dev/null +++ b/migration-tools/avatar-proxy/config.example.json @@ -0,0 +1,7 @@ +{ + "old_avatar_base": "https://pronounscc-legacy.your-s3-host.com/", + "new_avatar_base": "http://pronounscc.your-s3-host.com/", + "flag_base": "http://pronounscc.your-s3-host.com/", + "port": 6100, + "database": "postgresql://postgres:password@localhost/postgres" +} diff --git a/migration-tools/avatar-proxy/go.mod b/migration-tools/avatar-proxy/go.mod new file mode 100644 index 0000000..3009181 --- /dev/null +++ b/migration-tools/avatar-proxy/go.mod @@ -0,0 +1,17 @@ +module code.vulpine.solutions/sam/Foxnouns.NET/migration-tools/avatar-proxy + +go 1.24.2 + +require ( + github.com/go-chi/chi/v5 v5.2.1 + github.com/jackc/pgx/v5 v5.7.4 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/migration-tools/avatar-proxy/go.sum b/migration-tools/avatar-proxy/go.sum new file mode 100644 index 0000000..b7ffae7 --- /dev/null +++ b/migration-tools/avatar-proxy/go.sum @@ -0,0 +1,30 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/migration-tools/avatar-proxy/main.go b/migration-tools/avatar-proxy/main.go new file mode 100644 index 0000000..f43f35a --- /dev/null +++ b/migration-tools/avatar-proxy/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "strconv" + + "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Proxy struct { + OldAvatarBase string `json:"old_avatar_base"` + NewAvatarBase string `json:"new_avatar_base"` + FlagBase string `json:"flag_base"` + + Port int `json:"port"` + Database string `json:"database"` + + db *pgxpool.Pool +} + +type EntityWithAvatar struct { + ID uint64 + LegacyID string + Avatar string + AvatarMigrated bool +} + +func main() { + b, err := os.ReadFile("config.json") + if err != nil { + log.Fatalln("error reading config:", err) + } + + p := &Proxy{} + err = json.Unmarshal(b, p) + if err != nil { + log.Fatalln("error parsing config:", err) + } + + p.db, err = pgxpool.New(context.Background(), p.Database) + if err != nil { + log.Fatalln("error connecting to database:", err) + } + + r := chi.NewRouter() + + r.HandleFunc(`/flags/{hash:[\da-f]+}.webp`, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, fmt.Sprintf("%s%s", p.FlagBase, r.URL.Path), http.StatusTemporaryRedirect) + }) + r.Get(`/members/{id:[\d]+}/avatars/{hash:[\da-f]+}.webp`, p.proxyHandler("member")) + r.Get(`/users/{id:[\d]+}/avatars/{hash:[\da-f]+}.webp`, p.proxyHandler("user")) + r.NotFound(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("file not found")) + }) + + log.Printf("serving on port %v", p.Port) + + err = http.ListenAndServe(":"+strconv.Itoa(p.Port), r) + if err != nil { + log.Fatalf("listening on port %v: %v", p.Port, err) + } +} + +func (p *Proxy) proxyHandler(avatarType string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + hash := chi.URLParam(r, "hash") + + var e EntityWithAvatar + // don't do this normally, kids. avatarType can only be "user" or "member" so it's fine here but this is a BAD idea otherwise. + err := p.db.QueryRow( + r.Context(), "SELECT id, legacy_id, avatar, avatar_migrated FROM "+avatarType+"s WHERE id = $1 AND avatar = $2", + id, hash, + ).Scan(&e.ID, &e.LegacyID, &e.Avatar, &e.AvatarMigrated) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("avatar not found")) + return + } + + log.Printf("error getting avatar for %s %s: %v", avatarType, id, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + return + } + + if e.AvatarMigrated { + http.Redirect(w, r, fmt.Sprintf("%s/%ss/%d/avatars/%s.webp", p.NewAvatarBase, avatarType, e.ID, e.Avatar), http.StatusTemporaryRedirect) + } else { + http.Redirect(w, r, fmt.Sprintf("%s/%ss/%s/%s.webp", p.OldAvatarBase, avatarType, e.LegacyID, e.Avatar), http.StatusTemporaryRedirect) + } + } +}