feat(backend): global notices
This commit is contained in:
		
							parent
							
								
									22be49976a
								
							
						
					
					
						commit
						b07f4b75c0
					
				
					 19 changed files with 1247 additions and 8 deletions
				
			
		|  | @ -13,20 +13,23 @@ | |||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| using System.Text.RegularExpressions; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Services.Caching; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers; | ||||
| 
 | ||||
| [Route("/api/v2/meta")] | ||||
| public partial class MetaController(Config config) : ApiControllerBase | ||||
| public partial class MetaController(Config config, NoticeCacheService noticeCache) | ||||
|     : ApiControllerBase | ||||
| { | ||||
|     private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)] | ||||
|     public IActionResult GetMeta() => | ||||
|     public async Task<IActionResult> GetMeta(CancellationToken ct = default) => | ||||
|         Ok( | ||||
|             new MetaResponse( | ||||
|                 Repository, | ||||
|  | @ -45,10 +48,14 @@ public partial class MetaController(Config config) : ApiControllerBase | |||
|                     ValidationUtils.MaxCustomPreferences, | ||||
|                     AuthUtils.MaxAuthMethodsPerType, | ||||
|                     FlagsController.MaxFlagCount | ||||
|                 ) | ||||
|                 ), | ||||
|                 Notice: NoticeResponse(await noticeCache.GetAsync(ct)) | ||||
|             ) | ||||
|         ); | ||||
| 
 | ||||
|     private static MetaNoticeResponse? NoticeResponse(Notice? notice) => | ||||
|         notice == null ? null : new MetaNoticeResponse(notice.Id, notice.Message); | ||||
| 
 | ||||
|     [HttpGet("page/{page}")] | ||||
|     public async Task<IActionResult> GetStaticPageAsync(string page, CancellationToken ct = default) | ||||
|     { | ||||
|  | @ -71,7 +78,7 @@ public partial class MetaController(Config config) : ApiControllerBase | |||
| 
 | ||||
|     [HttpGet("/api/v2/coffee")] | ||||
|     public IActionResult BrewCoffee() => | ||||
|         Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); | ||||
|         StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!"); | ||||
| 
 | ||||
|     [GeneratedRegex(@"^[a-z\-_]+$")]
 | ||||
|     private static partial Regex PageRegex(); | ||||
|  |  | |||
							
								
								
									
										77
									
								
								Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers.Moderation; | ||||
| 
 | ||||
| [Route("/api/v2/notices")] | ||||
| [Authorize("user.moderation")] | ||||
| [Limit(RequireModerator = true)] | ||||
| public class NoticesController( | ||||
|     DatabaseContext db, | ||||
|     UserRendererService userRenderer, | ||||
|     ISnowflakeGenerator snowflakeGenerator, | ||||
|     IClock clock | ||||
| ) : ApiControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     public async Task<IActionResult> GetNoticesAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         List<Notice> notices = await db | ||||
|             .Notices.Include(n => n.Author) | ||||
|             .OrderByDescending(n => n.Id) | ||||
|             .ToListAsync(ct); | ||||
|         return Ok(notices.Select(RenderNotice)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost] | ||||
|     public async Task<IActionResult> CreateNoticeAsync(CreateNoticeRequest req) | ||||
|     { | ||||
|         Instant now = clock.GetCurrentInstant(); | ||||
|         if (req.StartTime < now) | ||||
|         { | ||||
|             throw new ApiError.BadRequest( | ||||
|                 "Start time cannot be in the past", | ||||
|                 "start_time", | ||||
|                 req.StartTime | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (req.EndTime < now) | ||||
|         { | ||||
|             throw new ApiError.BadRequest( | ||||
|                 "End time cannot be in the past", | ||||
|                 "end_time", | ||||
|                 req.EndTime | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         var notice = new Notice | ||||
|         { | ||||
|             Id = snowflakeGenerator.GenerateSnowflake(), | ||||
|             Message = req.Message, | ||||
|             StartTime = req.StartTime ?? clock.GetCurrentInstant(), | ||||
|             EndTime = req.EndTime, | ||||
|             Author = CurrentUser!, | ||||
|         }; | ||||
| 
 | ||||
|         db.Add(notice); | ||||
|         await db.SaveChangesAsync(); | ||||
| 
 | ||||
|         return Ok(RenderNotice(notice)); | ||||
|     } | ||||
| 
 | ||||
|     private NoticeResponse RenderNotice(Notice notice) => | ||||
|         new( | ||||
|             notice.Id, | ||||
|             notice.Message, | ||||
|             notice.StartTime, | ||||
|             notice.EndTime, | ||||
|             userRenderer.RenderPartialUser(notice.Author) | ||||
|         ); | ||||
| } | ||||
|  | @ -281,6 +281,8 @@ public class UsersController( | |||
| 
 | ||||
|         if (req.HasProperty(nameof(req.DarkMode))) | ||||
|             user.Settings.DarkMode = req.DarkMode; | ||||
|         if (req.HasProperty(nameof(req.LastReadNotice))) | ||||
|             user.Settings.LastReadNotice = req.LastReadNotice; | ||||
| 
 | ||||
|         user.LastActive = clock.GetCurrentInstant(); | ||||
|         db.Update(user); | ||||
|  |  | |||
|  | @ -73,6 +73,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) | |||
|     public DbSet<Report> Reports { get; init; } = null!; | ||||
|     public DbSet<AuditLogEntry> AuditLog { get; init; } = null!; | ||||
|     public DbSet<Notification> Notifications { get; init; } = null!; | ||||
|     public DbSet<Notice> Notices { get; init; } = null!; | ||||
| 
 | ||||
|     protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) | ||||
|     { | ||||
|  |  | |||
							
								
								
									
										915
									
								
								Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										915
									
								
								Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,915 @@ | |||
| // <auto-generated /> | ||||
| 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("20250329131053_AddNotices")] | ||||
|     partial class AddNotices | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientSecret") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_secret"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.PrimitiveCollection<string[]>("RedirectUris") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("redirect_uris"); | ||||
| 
 | ||||
|                     b.PrimitiveCollection<string[]>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.PrimitiveCollection<string[]>("ClearedFields") | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("cleared_fields"); | ||||
| 
 | ||||
|                     b.Property<long>("ModeratorId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("moderator_id"); | ||||
| 
 | ||||
|                     b.Property<string>("ModeratorUsername") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("moderator_username"); | ||||
| 
 | ||||
|                     b.Property<string>("Reason") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("reason"); | ||||
| 
 | ||||
|                     b.Property<long?>("ReportId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("report_id"); | ||||
| 
 | ||||
|                     b.Property<long?>("TargetMemberId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("target_member_id"); | ||||
| 
 | ||||
|                     b.Property<string>("TargetMemberName") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("target_member_name"); | ||||
| 
 | ||||
|                     b.Property<long?>("TargetUserId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("target_user_id"); | ||||
| 
 | ||||
|                     b.Property<string>("TargetUsername") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("target_username"); | ||||
| 
 | ||||
|                     b.Property<int>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<int>("AuthType") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("auth_type"); | ||||
| 
 | ||||
|                     b.Property<long?>("FediverseApplicationId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("fediverse_application_id"); | ||||
| 
 | ||||
|                     b.Property<string>("RemoteId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("remote_id"); | ||||
| 
 | ||||
|                     b.Property<string>("RemoteUsername") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("remote_username"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("Filename") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("filename"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientSecret") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_secret"); | ||||
| 
 | ||||
|                     b.Property<string>("Domain") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("domain"); | ||||
| 
 | ||||
|                     b.Property<bool>("ForceRefresh") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("force_refresh"); | ||||
| 
 | ||||
|                     b.Property<int>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("Avatar") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("avatar"); | ||||
| 
 | ||||
|                     b.Property<string>("Bio") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("bio"); | ||||
| 
 | ||||
|                     b.Property<string>("DisplayName") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("display_name"); | ||||
| 
 | ||||
|                     b.Property<List<Field>>("Fields") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("fields"); | ||||
| 
 | ||||
|                     b.Property<string>("LegacyId") | ||||
|                         .IsRequired() | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("legacy_id") | ||||
|                         .HasDefaultValueSql("gen_random_uuid()"); | ||||
| 
 | ||||
|                     b.PrimitiveCollection<string[]>("Links") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("links"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<List<FieldEntry>>("Names") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("names"); | ||||
| 
 | ||||
|                     b.Property<List<Pronoun>>("Pronouns") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("pronouns"); | ||||
| 
 | ||||
|                     b.Property<string>("Sid") | ||||
|                         .IsRequired() | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("sid") | ||||
|                         .HasDefaultValueSql("find_free_member_sid()"); | ||||
| 
 | ||||
|                     b.Property<bool>("Unlisted") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("unlisted"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
| 
 | ||||
|                     b.Property<long>("MemberId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("member_id"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<long>("AuthorId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("author_id"); | ||||
| 
 | ||||
|                     b.Property<Instant>("EndTime") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("end_time"); | ||||
| 
 | ||||
|                     b.Property<string>("Message") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("message"); | ||||
| 
 | ||||
|                     b.Property<Instant>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<Instant?>("AcknowledgedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("acknowledged_at"); | ||||
| 
 | ||||
|                     b.Property<string>("LocalizationKey") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("localization_key"); | ||||
| 
 | ||||
|                     b.Property<Dictionary<string, string>>("LocalizationParams") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("hstore") | ||||
|                         .HasColumnName("localization_params"); | ||||
| 
 | ||||
|                     b.Property<string>("Message") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("message"); | ||||
| 
 | ||||
|                     b.Property<long>("TargetId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("target_id"); | ||||
| 
 | ||||
|                     b.Property<int>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("Description") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("hash"); | ||||
| 
 | ||||
|                     b.Property<string>("LegacyId") | ||||
|                         .IsRequired() | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("legacy_id") | ||||
|                         .HasDefaultValueSql("gen_random_uuid()"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("Context") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("context"); | ||||
| 
 | ||||
|                     b.Property<int>("Reason") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("reason"); | ||||
| 
 | ||||
|                     b.Property<long>("ReporterId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("reporter_id"); | ||||
| 
 | ||||
|                     b.Property<int>("Status") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("status"); | ||||
| 
 | ||||
|                     b.Property<long?>("TargetMemberId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("target_member_id"); | ||||
| 
 | ||||
|                     b.Property<string>("TargetSnapshot") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("target_snapshot"); | ||||
| 
 | ||||
|                     b.Property<int>("TargetType") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("target_type"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<long>("ApplicationId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("application_id"); | ||||
| 
 | ||||
|                     b.Property<Instant>("ExpiresAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expires_at"); | ||||
| 
 | ||||
|                     b.Property<byte[]>("Hash") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("hash"); | ||||
| 
 | ||||
|                     b.Property<bool>("ManuallyExpired") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("manually_expired"); | ||||
| 
 | ||||
|                     b.PrimitiveCollection<string[]>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("scopes"); | ||||
| 
 | ||||
|                     b.Property<long>("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<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("Avatar") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("avatar"); | ||||
| 
 | ||||
|                     b.Property<string>("Bio") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("bio"); | ||||
| 
 | ||||
|                     b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("custom_preferences"); | ||||
| 
 | ||||
|                     b.Property<bool>("Deleted") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("deleted"); | ||||
| 
 | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
| 
 | ||||
|                     b.Property<long?>("DeletedBy") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("deleted_by"); | ||||
| 
 | ||||
|                     b.Property<string>("DisplayName") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("display_name"); | ||||
| 
 | ||||
|                     b.Property<List<Field>>("Fields") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("fields"); | ||||
| 
 | ||||
|                     b.Property<Instant>("LastActive") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("last_active"); | ||||
| 
 | ||||
|                     b.Property<Instant>("LastSidReroll") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("last_sid_reroll"); | ||||
| 
 | ||||
|                     b.Property<string>("LegacyId") | ||||
|                         .IsRequired() | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("legacy_id") | ||||
|                         .HasDefaultValueSql("gen_random_uuid()"); | ||||
| 
 | ||||
|                     b.PrimitiveCollection<string[]>("Links") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("links"); | ||||
| 
 | ||||
|                     b.Property<bool>("ListHidden") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("list_hidden"); | ||||
| 
 | ||||
|                     b.Property<string>("MemberTitle") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("member_title"); | ||||
| 
 | ||||
|                     b.Property<List<FieldEntry>>("Names") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("names"); | ||||
| 
 | ||||
|                     b.Property<string>("Password") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("password"); | ||||
| 
 | ||||
|                     b.Property<List<Pronoun>>("Pronouns") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("pronouns"); | ||||
| 
 | ||||
|                     b.Property<int>("Role") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("role"); | ||||
| 
 | ||||
|                     b.Property<UserSettings>("Settings") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("settings"); | ||||
| 
 | ||||
|                     b.Property<string>("Sid") | ||||
|                         .IsRequired() | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("sid") | ||||
|                         .HasDefaultValueSql("find_free_user_sid()"); | ||||
| 
 | ||||
|                     b.Property<string>("Timezone") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("timezone"); | ||||
| 
 | ||||
|                     b.Property<string>("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<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
| 
 | ||||
|                     b.Property<long>("PrideFlagId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("pride_flag_id"); | ||||
| 
 | ||||
|                     b.Property<long>("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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,56 @@ | |||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Database.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddNotices : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "notices", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<long>(type: "bigint", nullable: false), | ||||
|                     message = table.Column<string>(type: "text", nullable: false), | ||||
|                     start_time = table.Column<Instant>( | ||||
|                         type: "timestamp with time zone", | ||||
|                         nullable: false | ||||
|                     ), | ||||
|                     end_time = table.Column<Instant>( | ||||
|                         type: "timestamp with time zone", | ||||
|                         nullable: false | ||||
|                     ), | ||||
|                     author_id = table.Column<long>(type: "bigint", nullable: false), | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_notices", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_notices_users_author_id", | ||||
|                         column: x => x.author_id, | ||||
|                         principalTable: "users", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade | ||||
|                     ); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_notices_author_id", | ||||
|                 table: "notices", | ||||
|                 column: "author_id" | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable(name: "notices"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -343,6 +343,38 @@ namespace Foxnouns.Backend.Database.Migrations | |||
|                     b.ToTable("member_flags", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<long>("AuthorId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("author_id"); | ||||
| 
 | ||||
|                     b.Property<Instant>("EndTime") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("end_time"); | ||||
| 
 | ||||
|                     b.Property<string>("Message") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("message"); | ||||
| 
 | ||||
|                     b.Property<Instant>("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<long>("Id") | ||||
|  | @ -750,6 +782,18 @@ namespace Foxnouns.Backend.Database.Migrations | |||
|                     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") | ||||
|  |  | |||
							
								
								
									
										13
									
								
								Foxnouns.Backend/Database/Models/Notice.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Foxnouns.Backend/Database/Models/Notice.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Database.Models; | ||||
| 
 | ||||
| public class Notice : BaseModel | ||||
| { | ||||
|     public required string Message { get; set; } | ||||
|     public required Instant StartTime { get; set; } | ||||
|     public required Instant EndTime { get; set; } | ||||
| 
 | ||||
|     public Snowflake AuthorId { get; init; } | ||||
|     public User Author { get; init; } = null!; | ||||
| } | ||||
|  | @ -95,4 +95,5 @@ public enum PreferenceSize | |||
| public class UserSettings | ||||
| { | ||||
|     public bool? DarkMode { get; set; } | ||||
|     public Snowflake? LastReadNotice { get; set; } | ||||
| } | ||||
|  |  | |||
|  | @ -14,6 +14,8 @@ | |||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| using Foxnouns.Backend.Database; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record MetaResponse( | ||||
|  | @ -22,9 +24,12 @@ public record MetaResponse( | |||
|     string Hash, | ||||
|     int Members, | ||||
|     UserInfoResponse Users, | ||||
|     LimitsResponse Limits | ||||
|     LimitsResponse Limits, | ||||
|     MetaNoticeResponse? Notice | ||||
| ); | ||||
| 
 | ||||
| public record MetaNoticeResponse(Snowflake Id, string Message); | ||||
| 
 | ||||
| public record UserInfoResponse(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); | ||||
| 
 | ||||
| public record LimitsResponse( | ||||
|  |  | |||
|  | @ -122,3 +122,13 @@ public record QueryUserResponse( | |||
| ); | ||||
| 
 | ||||
| public record QuerySensitiveUserDataRequest(string Reason); | ||||
| 
 | ||||
| public record NoticeResponse( | ||||
|     Snowflake Id, | ||||
|     string Message, | ||||
|     Instant StartTime, | ||||
|     Instant EndTime, | ||||
|     PartialUser Author | ||||
| ); | ||||
| 
 | ||||
| public record CreateNoticeRequest(string Message, Instant? StartTime, Instant EndTime); | ||||
|  |  | |||
|  | @ -80,6 +80,7 @@ public record PartialUser( | |||
| public class UpdateUserSettingsRequest : PatchRequest | ||||
| { | ||||
|     public bool? DarkMode { get; init; } | ||||
|     public Snowflake? LastReadNotice { get; init; } | ||||
| } | ||||
| 
 | ||||
| public class CustomPreferenceUpdateRequest | ||||
|  |  | |||
|  | @ -15,10 +15,12 @@ | |||
| using Coravel; | ||||
| using Coravel.Queuing.Interfaces; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Jobs; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Foxnouns.Backend.Services.Auth; | ||||
| using Foxnouns.Backend.Services.Caching; | ||||
| using Foxnouns.Backend.Services.V1; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Http.Resilience; | ||||
|  | @ -162,6 +164,7 @@ public static class WebApplicationExtensions | |||
|                     .AddScoped<ObjectStorageService>() | ||||
|                     .AddTransient<DataCleanupService>() | ||||
|                     .AddTransient<ValidationService>() | ||||
|                     .AddSingleton<NoticeCacheService>() | ||||
|                     // Background services | ||||
|                     .AddHostedService<PeriodicTasksService>() | ||||
|                     // Transient jobs | ||||
|  |  | |||
|  | @ -253,14 +253,14 @@ public class AuthService( | |||
|     { | ||||
|         AssertValidAuthType(authType, app); | ||||
| 
 | ||||
|         // This is already checked when | ||||
|         // This is already checked when generating an add account state, but we check it here too just in case. | ||||
|         int currentCount = await db | ||||
|             .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) | ||||
|             .CountAsync(ct); | ||||
|         if (currentCount >= AuthUtils.MaxAuthMethodsPerType) | ||||
|         { | ||||
|             throw new ApiError.BadRequest( | ||||
|                 "Too many linked accounts of this type, maximum of 3 per account." | ||||
|                 $"Too many linked accounts of this type, maximum of {AuthUtils.MaxAuthMethodsPerType} per account." | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										39
									
								
								Foxnouns.Backend/Services/Caching/NoticeCacheService.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Foxnouns.Backend/Services/Caching/NoticeCacheService.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| // Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published | ||||
| // by the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Services.Caching; | ||||
| 
 | ||||
| public class NoticeCacheService(IServiceProvider serviceProvider, IClock clock, ILogger logger) | ||||
|     : SingletonCacheService<Notice>(serviceProvider, clock, logger) | ||||
| { | ||||
|     public override Duration MaxAge { get; init; } = Duration.FromMinutes(5); | ||||
| 
 | ||||
|     public override Func< | ||||
|         DatabaseContext, | ||||
|         CancellationToken, | ||||
|         Task<Notice?> | ||||
|     > FetchFunc { get; init; } = | ||||
|         async (db, ct) => | ||||
|             await db | ||||
|                 .Notices.Where(n => | ||||
|                     n.StartTime < clock.GetCurrentInstant() && n.EndTime > clock.GetCurrentInstant() | ||||
|                 ) | ||||
|                 .OrderByDescending(n => n.Id) | ||||
|                 .FirstOrDefaultAsync(ct); | ||||
| } | ||||
							
								
								
									
										63
									
								
								Foxnouns.Backend/Services/Caching/SingletonCacheService.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Foxnouns.Backend/Services/Caching/SingletonCacheService.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| // Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published | ||||
| // by the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| using Foxnouns.Backend.Database; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Services.Caching; | ||||
| 
 | ||||
| public abstract class SingletonCacheService<T>( | ||||
|     IServiceProvider serviceProvider, | ||||
|     IClock clock, | ||||
|     ILogger logger | ||||
| ) | ||||
|     where T : class | ||||
| { | ||||
|     private T? _item; | ||||
|     private Instant _lastUpdated = Instant.MinValue; | ||||
|     private readonly SemaphoreSlim _semaphore = new(1, 1); | ||||
|     private readonly ILogger _logger = logger.ForContext<SingletonCacheService<T>>(); | ||||
| 
 | ||||
|     public virtual Duration MaxAge { get; init; } = Duration.FromMinutes(5); | ||||
| 
 | ||||
|     public virtual Func<DatabaseContext, CancellationToken, Task<T?>> FetchFunc { get; init; } = | ||||
|         (_, __) => Task.FromResult<T?>(null); | ||||
| 
 | ||||
|     public async Task<T?> GetAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         await _semaphore.WaitAsync(ct); | ||||
|         try | ||||
|         { | ||||
|             if (_lastUpdated > clock.GetCurrentInstant() - MaxAge) | ||||
|             { | ||||
|                 return _item; | ||||
|             } | ||||
| 
 | ||||
|             _logger.Debug("Cached item of type {Type} is expired, fetching it.", typeof(T)); | ||||
| 
 | ||||
|             await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); | ||||
|             await using DatabaseContext db = | ||||
|                 scope.ServiceProvider.GetRequiredService<DatabaseContext>(); | ||||
| 
 | ||||
|             T? item = await FetchFunc(db, ct); | ||||
|             _item = item; | ||||
|             _lastUpdated = clock.GetCurrentInstant(); | ||||
|             return item; | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             _semaphore.Release(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue