| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332 |
- using System;
- using System.Collections.Generic;
- using System.Collections.Immutable;
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Net;
- using System.Text.Json;
- using Content.Shared.Database;
- using Microsoft.EntityFrameworkCore;
- using NpgsqlTypes;
- namespace Content.Server.Database
- {
- public abstract class ServerDbContext : DbContext
- {
- protected ServerDbContext(DbContextOptions options) : base(options)
- {
- }
- public DbSet<Preference> Preference { get; set; } = null!;
- public DbSet<Profile> Profile { get; set; } = null!;
- public DbSet<AssignedUserId> AssignedUserId { get; set; } = null!;
- public DbSet<Player> Player { get; set; } = default!;
- public DbSet<Admin> Admin { get; set; } = null!;
- public DbSet<AdminRank> AdminRank { get; set; } = null!;
- public DbSet<Round> Round { get; set; } = null!;
- public DbSet<Server> Server { get; set; } = null!;
- public DbSet<AdminLog> AdminLog { get; set; } = null!;
- public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!;
- public DbSet<Whitelist> Whitelist { get; set; } = null!;
- public DbSet<Blacklist> Blacklist { get; set; } = null!;
- public DbSet<ServerBan> Ban { get; set; } = default!;
- public DbSet<ServerUnban> Unban { get; set; } = default!;
- public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
- public DbSet<ConnectionLog> ConnectionLog { get; set; } = default!;
- public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!;
- public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
- public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!;
- public DbSet<PlayTime> PlayTime { get; set; } = default!;
- public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!;
- public DbSet<AdminNote> AdminNotes { get; set; } = null!;
- public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!;
- public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
- public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
- public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
- public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Preference>()
- .HasIndex(p => p.UserId)
- .IsUnique();
- modelBuilder.Entity<Profile>()
- .HasIndex(p => new {p.Slot, PrefsId = p.PreferenceId})
- .IsUnique();
- modelBuilder.Entity<Antag>()
- .HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.AntagName})
- .IsUnique();
- modelBuilder.Entity<Trait>()
- .HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
- .IsUnique();
- modelBuilder.Entity<ProfileRoleLoadout>()
- .HasOne(e => e.Profile)
- .WithMany(e => e.Loadouts)
- .HasForeignKey(e => e.ProfileId)
- .IsRequired();
- modelBuilder.Entity<ProfileLoadoutGroup>()
- .HasOne(e => e.ProfileRoleLoadout)
- .WithMany(e => e.Groups)
- .HasForeignKey(e => e.ProfileRoleLoadoutId)
- .IsRequired();
- modelBuilder.Entity<ProfileLoadout>()
- .HasOne(e => e.ProfileLoadoutGroup)
- .WithMany(e => e.Loadouts)
- .HasForeignKey(e => e.ProfileLoadoutGroupId)
- .IsRequired();
- modelBuilder.Entity<Job>()
- .HasIndex(j => j.ProfileId);
- modelBuilder.Entity<Job>()
- .HasIndex(j => j.ProfileId, "IX_job_one_high_priority")
- .IsUnique()
- .HasFilter("priority = 3");
- modelBuilder.Entity<Job>()
- .HasIndex(j => new { j.ProfileId, j.JobName })
- .IsUnique();
- modelBuilder.Entity<AssignedUserId>()
- .HasIndex(p => p.UserName)
- .IsUnique();
- // Can't have two usernames with the same user ID.
- modelBuilder.Entity<AssignedUserId>()
- .HasIndex(p => p.UserId)
- .IsUnique();
- modelBuilder.Entity<Admin>()
- .HasOne(p => p.AdminRank)
- .WithMany(p => p!.Admins)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminFlag>()
- .HasIndex(f => new {f.Flag, f.AdminId})
- .IsUnique();
- modelBuilder.Entity<AdminRankFlag>()
- .HasIndex(f => new {f.Flag, f.AdminRankId})
- .IsUnique();
- modelBuilder.Entity<AdminLog>()
- .HasKey(log => new {log.RoundId, log.Id});
- modelBuilder.Entity<AdminLog>()
- .Property(log => log.Id);
- modelBuilder.Entity<AdminLog>()
- .HasIndex(log => log.Date);
- modelBuilder.Entity<PlayTime>()
- .HasIndex(v => new { v.PlayerId, Role = v.Tracker })
- .IsUnique();
- modelBuilder.Entity<AdminLogPlayer>()
- .HasOne(player => player.Player)
- .WithMany(player => player.AdminLogs)
- .HasForeignKey(player => player.PlayerUserId)
- .HasPrincipalKey(player => player.UserId);
- modelBuilder.Entity<AdminLogPlayer>()
- .HasIndex(p => p.PlayerUserId);
- modelBuilder.Entity<Round>()
- .HasIndex(round => round.StartDate);
- modelBuilder.Entity<AdminLogPlayer>()
- .HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId});
- modelBuilder.Entity<ServerBan>()
- .HasIndex(p => p.PlayerUserId);
- modelBuilder.Entity<ServerBan>()
- .HasIndex(p => p.Address);
- modelBuilder.Entity<ServerBan>()
- .HasIndex(p => p.PlayerUserId);
- modelBuilder.Entity<ServerUnban>()
- .HasIndex(p => p.BanId)
- .IsUnique();
- modelBuilder.Entity<ServerBan>().ToTable(t =>
- t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
- // Ban exemption can't have flags 0 since that wouldn't exempt anything.
- // The row should be removed if setting to 0.
- modelBuilder.Entity<ServerBanExemption>().ToTable(t =>
- t.HasCheckConstraint("FlagsNotZero", "flags != 0"));
- modelBuilder.Entity<ServerRoleBan>()
- .HasIndex(p => p.PlayerUserId);
- modelBuilder.Entity<ServerRoleBan>()
- .HasIndex(p => p.Address);
- modelBuilder.Entity<ServerRoleBan>()
- .HasIndex(p => p.PlayerUserId);
- modelBuilder.Entity<ServerRoleUnban>()
- .HasIndex(p => p.BanId)
- .IsUnique();
- modelBuilder.Entity<ServerRoleBan>().ToTable(t =>
- t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
- modelBuilder.Entity<Player>()
- .HasIndex(p => p.UserId)
- .IsUnique();
- modelBuilder.Entity<Player>()
- .HasIndex(p => p.LastSeenUserName);
- modelBuilder.Entity<ConnectionLog>()
- .HasIndex(p => p.UserId);
- modelBuilder.Entity<ConnectionLog>()
- .HasIndex(p => p.Time);
- modelBuilder.Entity<ConnectionLog>()
- .Property(p => p.ServerId)
- .HasDefaultValue(0);
- modelBuilder.Entity<ConnectionLog>()
- .HasOne(p => p.Server)
- .WithMany(p => p.ConnectionLogs)
- .OnDelete(DeleteBehavior.SetNull);
- // SetNull is necessary for created by/edited by-s here,
- // so you can safely delete admins (GDPR right to erasure) while keeping the notes intact
- modelBuilder.Entity<AdminNote>()
- .HasOne(note => note.Player)
- .WithMany(player => player.AdminNotesReceived)
- .HasForeignKey(note => note.PlayerUserId)
- .HasPrincipalKey(player => player.UserId)
- .OnDelete(DeleteBehavior.Cascade);
- modelBuilder.Entity<AdminNote>()
- .HasOne(version => version.CreatedBy)
- .WithMany(author => author.AdminNotesCreated)
- .HasForeignKey(note => note.CreatedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminNote>()
- .HasOne(version => version.LastEditedBy)
- .WithMany(author => author.AdminNotesLastEdited)
- .HasForeignKey(note => note.LastEditedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminNote>()
- .HasOne(version => version.DeletedBy)
- .WithMany(author => author.AdminNotesDeleted)
- .HasForeignKey(note => note.DeletedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminWatchlist>()
- .HasOne(note => note.Player)
- .WithMany(player => player.AdminWatchlistsReceived)
- .HasForeignKey(note => note.PlayerUserId)
- .HasPrincipalKey(player => player.UserId)
- .OnDelete(DeleteBehavior.Cascade);
- modelBuilder.Entity<AdminWatchlist>()
- .HasOne(version => version.CreatedBy)
- .WithMany(author => author.AdminWatchlistsCreated)
- .HasForeignKey(note => note.CreatedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminWatchlist>()
- .HasOne(version => version.LastEditedBy)
- .WithMany(author => author.AdminWatchlistsLastEdited)
- .HasForeignKey(note => note.LastEditedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminWatchlist>()
- .HasOne(version => version.DeletedBy)
- .WithMany(author => author.AdminWatchlistsDeleted)
- .HasForeignKey(note => note.DeletedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminMessage>()
- .HasOne(note => note.Player)
- .WithMany(player => player.AdminMessagesReceived)
- .HasForeignKey(note => note.PlayerUserId)
- .HasPrincipalKey(player => player.UserId)
- .OnDelete(DeleteBehavior.Cascade);
- modelBuilder.Entity<AdminMessage>()
- .HasOne(version => version.CreatedBy)
- .WithMany(author => author.AdminMessagesCreated)
- .HasForeignKey(note => note.CreatedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminMessage>()
- .HasOne(version => version.LastEditedBy)
- .WithMany(author => author.AdminMessagesLastEdited)
- .HasForeignKey(note => note.LastEditedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<AdminMessage>()
- .HasOne(version => version.DeletedBy)
- .WithMany(author => author.AdminMessagesDeleted)
- .HasForeignKey(note => note.DeletedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- // A message cannot be "dismissed" without also being "seen".
- modelBuilder.Entity<AdminMessage>().ToTable(t =>
- t.HasCheckConstraint("NotDismissedAndSeen",
- "NOT dismissed OR seen"));
- modelBuilder.Entity<ServerBan>()
- .HasOne(ban => ban.CreatedBy)
- .WithMany(author => author.AdminServerBansCreated)
- .HasForeignKey(ban => ban.BanningAdmin)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<ServerBan>()
- .HasOne(ban => ban.LastEditedBy)
- .WithMany(author => author.AdminServerBansLastEdited)
- .HasForeignKey(ban => ban.LastEditedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<ServerRoleBan>()
- .HasOne(ban => ban.CreatedBy)
- .WithMany(author => author.AdminServerRoleBansCreated)
- .HasForeignKey(ban => ban.BanningAdmin)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<ServerRoleBan>()
- .HasOne(ban => ban.LastEditedBy)
- .WithMany(author => author.AdminServerRoleBansLastEdited)
- .HasForeignKey(ban => ban.LastEditedById)
- .HasPrincipalKey(author => author.UserId)
- .OnDelete(DeleteBehavior.SetNull);
- modelBuilder.Entity<RoleWhitelist>()
- .HasOne(w => w.Player)
- .WithMany(p => p.JobWhitelists)
- .HasForeignKey(w => w.PlayerUserId)
- .HasPrincipalKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
- // Changes for modern HWID integration
- modelBuilder.Entity<Player>()
- .OwnsOne(p => p.LastSeenHWId)
- .Property(p => p.Hwid)
- .HasColumnName("last_seen_hwid");
- modelBuilder.Entity<Player>()
- .OwnsOne(p => p.LastSeenHWId)
- .Property(p => p.Type)
- .HasDefaultValue(HwidType.Legacy);
- modelBuilder.Entity<ServerBan>()
- .OwnsOne(p => p.HWId)
- .Property(p => p.Hwid)
- .HasColumnName("hwid");
- modelBuilder.Entity<ServerBan>()
- .OwnsOne(p => p.HWId)
- .Property(p => p.Type)
- .HasDefaultValue(HwidType.Legacy);
- modelBuilder.Entity<ServerRoleBan>()
- .OwnsOne(p => p.HWId)
- .Property(p => p.Hwid)
- .HasColumnName("hwid");
- modelBuilder.Entity<ServerRoleBan>()
- .OwnsOne(p => p.HWId)
- .Property(p => p.Type)
- .HasDefaultValue(HwidType.Legacy);
- modelBuilder.Entity<ConnectionLog>()
- .OwnsOne(p => p.HWId)
- .Property(p => p.Hwid)
- .HasColumnName("hwid");
- modelBuilder.Entity<ConnectionLog>()
- .OwnsOne(p => p.HWId)
- .Property(p => p.Type)
- .HasDefaultValue(HwidType.Legacy);
- }
- public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
- {
- return query.Where(log => EF.Functions.Like(log.Message, "%" + searchText + "%"));
- }
- public abstract int CountAdminLogs();
- }
- public class Preference
- {
- // NOTE: on postgres there SHOULD be an FK ensuring that the selected character slot always exists.
- // I had to use a migration to implement it and as a result its creation is a finicky mess.
- // Because if I let EFCore know about it it would explode on a circular reference.
- // Also it has to be DEFERRABLE INITIALLY DEFERRED so that insertion of new preferences works.
- // Also I couldn't figure out how to create it on SQLite.
- public int Id { get; set; }
- public Guid UserId { get; set; }
- public int SelectedCharacterSlot { get; set; }
- public string AdminOOCColor { get; set; } = null!;
- public List<Profile> Profiles { get; } = new();
- }
- public class Profile
- {
- public int Id { get; set; }
- public int Slot { get; set; }
- [Column("char_name")] public string CharacterName { get; set; } = null!;
- public string FlavorText { get; set; } = null!;
- public int Age { get; set; }
- public string Sex { get; set; } = null!;
- public string Gender { get; set; } = null!;
- public string Species { get; set; } = null!;
- [Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
- public string HairName { get; set; } = null!;
- public string HairColor { get; set; } = null!;
- public string FacialHairName { get; set; } = null!;
- public string FacialHairColor { get; set; } = null!;
- public string EyeColor { get; set; } = null!;
- public string SkinColor { get; set; } = null!;
- public int SpawnPriority { get; set; } = 0;
- public List<Job> Jobs { get; } = new();
- public List<Antag> Antags { get; } = new();
- public List<Trait> Traits { get; } = new();
- public List<ProfileRoleLoadout> Loadouts { get; } = new();
- [Column("pref_unavailable")] public DbPreferenceUnavailableMode PreferenceUnavailable { get; set; }
- public int PreferenceId { get; set; }
- public Preference Preference { get; set; } = null!;
- }
- public class Job
- {
- public int Id { get; set; }
- public Profile Profile { get; set; } = null!;
- public int ProfileId { get; set; }
- public string JobName { get; set; } = null!;
- public DbJobPriority Priority { get; set; }
- }
- public enum DbJobPriority
- {
- // These enum values HAVE to match the ones in JobPriority in Content.Shared
- Never = 0,
- Low = 1,
- Medium = 2,
- High = 3
- }
- public class Antag
- {
- public int Id { get; set; }
- public Profile Profile { get; set; } = null!;
- public int ProfileId { get; set; }
- public string AntagName { get; set; } = null!;
- }
- public class Trait
- {
- public int Id { get; set; }
- public Profile Profile { get; set; } = null!;
- public int ProfileId { get; set; }
- public string TraitName { get; set; } = null!;
- }
- #region Loadouts
- /// <summary>
- /// Corresponds to a single role's loadout inside the DB.
- /// </summary>
- public class ProfileRoleLoadout
- {
- public int Id { get; set; }
- public int ProfileId { get; set; }
- public Profile Profile { get; set; } = null!;
- /// <summary>
- /// The corresponding role prototype on the profile.
- /// </summary>
- public string RoleName { get; set; } = string.Empty;
- /// <summary>
- /// Custom name of the role loadout if it supports it.
- /// </summary>
- [MaxLength(256)]
- public string? EntityName { get; set; }
- /// <summary>
- /// Store the saved loadout groups. These may get validated and removed when loaded at runtime.
- /// </summary>
- public List<ProfileLoadoutGroup> Groups { get; set; } = new();
- }
- /// <summary>
- /// Corresponds to a loadout group prototype with the specified loadouts attached.
- /// </summary>
- public class ProfileLoadoutGroup
- {
- public int Id { get; set; }
- public int ProfileRoleLoadoutId { get; set; }
- /// <summary>
- /// The corresponding RoleLoadout that owns this.
- /// </summary>
- public ProfileRoleLoadout ProfileRoleLoadout { get; set; } = null!;
- /// <summary>
- /// The corresponding group prototype.
- /// </summary>
- public string GroupName { get; set; } = string.Empty;
- /// <summary>
- /// Selected loadout prototype. Null if none is set.
- /// May get validated at runtime and updated to to the default.
- /// </summary>
- public List<ProfileLoadout> Loadouts { get; set; } = new();
- }
- /// <summary>
- /// Corresponds to a selected loadout.
- /// </summary>
- public class ProfileLoadout
- {
- public int Id { get; set; }
- public int ProfileLoadoutGroupId { get; set; }
- public ProfileLoadoutGroup ProfileLoadoutGroup { get; set; } = null!;
- /// <summary>
- /// Corresponding loadout prototype.
- /// </summary>
- public string LoadoutName { get; set; } = string.Empty;
- /*
- * Insert extra data here like custom descriptions or colors or whatever.
- */
- }
- #endregion
- public enum DbPreferenceUnavailableMode
- {
- // These enum values HAVE to match the ones in PreferenceUnavailableMode in Shared.
- StayInLobby = 0,
- SpawnAsOverflow,
- }
- public class AssignedUserId
- {
- public int Id { get; set; }
- public string UserName { get; set; } = null!;
- public Guid UserId { get; set; }
- }
- [Table("player")]
- public class Player
- {
- public int Id { get; set; }
- // Permanent data
- public Guid UserId { get; set; }
- public DateTime FirstSeenTime { get; set; }
- // Data that gets updated on each join.
- public string LastSeenUserName { get; set; } = null!;
- public DateTime LastSeenTime { get; set; }
- public IPAddress LastSeenAddress { get; set; } = null!;
- public TypedHwid? LastSeenHWId { get; set; }
- // Data that changes with each round
- public List<Round> Rounds { get; set; } = null!;
- public List<AdminLogPlayer> AdminLogs { get; set; } = null!;
- public DateTime? LastReadRules { get; set; }
- public List<AdminNote> AdminNotesReceived { get; set; } = null!;
- public List<AdminNote> AdminNotesCreated { get; set; } = null!;
- public List<AdminNote> AdminNotesLastEdited { get; set; } = null!;
- public List<AdminNote> AdminNotesDeleted { get; set; } = null!;
- public List<AdminWatchlist> AdminWatchlistsReceived { get; set; } = null!;
- public List<AdminWatchlist> AdminWatchlistsCreated { get; set; } = null!;
- public List<AdminWatchlist> AdminWatchlistsLastEdited { get; set; } = null!;
- public List<AdminWatchlist> AdminWatchlistsDeleted { get; set; } = null!;
- public List<AdminMessage> AdminMessagesReceived { get; set; } = null!;
- public List<AdminMessage> AdminMessagesCreated { get; set; } = null!;
- public List<AdminMessage> AdminMessagesLastEdited { get; set; } = null!;
- public List<AdminMessage> AdminMessagesDeleted { get; set; } = null!;
- public List<ServerBan> AdminServerBansCreated { get; set; } = null!;
- public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
- public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
- public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
- public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
- }
- [Table("whitelist")]
- public class Whitelist
- {
- [Required, Key] public Guid UserId { get; set; }
- }
- /// <summary>
- /// List of users who are on the "blacklist". This is a list that may be used by Whitelist implementations to deny access to certain users.
- /// </summary>
- [Table("blacklist")]
- public class Blacklist
- {
- [Required, Key] public Guid UserId { get; set; }
- }
- public class Admin
- {
- [Key] public Guid UserId { get; set; }
- public string? Title { get; set; }
- /// <summary>
- /// If true, the admin is voluntarily deadminned. They can re-admin at any time.
- /// </summary>
- public bool Deadminned { get; set; }
- /// <summary>
- /// If true, the admin is suspended by an admin with <c>PERMISSIONS</c>. They will not have in-game permissions.
- /// </summary>
- public bool Suspended { get; set; }
- public int? AdminRankId { get; set; }
- public AdminRank? AdminRank { get; set; }
- public List<AdminFlag> Flags { get; set; } = default!;
- }
- public class AdminFlag
- {
- public int Id { get; set; }
- public string Flag { get; set; } = default!;
- public bool Negative { get; set; }
- public Guid AdminId { get; set; }
- public Admin Admin { get; set; } = default!;
- }
- public class AdminRank
- {
- public int Id { get; set; }
- public string Name { get; set; } = default!;
- public List<Admin> Admins { get; set; } = default!;
- public List<AdminRankFlag> Flags { get; set; } = default!;
- }
- public class AdminRankFlag
- {
- public int Id { get; set; }
- public string Flag { get; set; } = default!;
- public int AdminRankId { get; set; }
- public AdminRank Rank { get; set; } = default!;
- }
- public class Round
- {
- [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; set; }
- public DateTime? StartDate { get; set; }
- public List<Player> Players { get; set; } = default!;
- public List<AdminLog> AdminLogs { get; set; } = default!;
- [ForeignKey("Server")] public int ServerId { get; set; }
- public Server Server { get; set; } = default!;
- }
- public class Server
- {
- [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; set; }
- public string Name { get; set; } = default!;
- [InverseProperty(nameof(Round.Server))]
- public List<Round> Rounds { get; set; } = default!;
- [InverseProperty(nameof(ConnectionLog.Server))]
- public List<ConnectionLog> ConnectionLogs { get; set; } = default!;
- }
- [Index(nameof(Type))]
- public class AdminLog
- {
- [Key, ForeignKey("Round")] public int RoundId { get; set; }
- [Key]
- public int Id { get; set; }
- public Round Round { get; set; } = default!;
- [Required] public LogType Type { get; set; }
- [Required] public LogImpact Impact { get; set; }
- [Required] public DateTime Date { get; set; }
- [Required] public string Message { get; set; } = default!;
- [Required, Column(TypeName = "jsonb")] public JsonDocument Json { get; set; } = default!;
- public List<AdminLogPlayer> Players { get; set; } = default!;
- }
- public class AdminLogPlayer
- {
- [Required, Key] public int RoundId { get; set; }
- [Required, Key] public int LogId { get; set; }
- [Required, Key, ForeignKey("Player")] public Guid PlayerUserId { get; set; }
- public Player Player { get; set; } = default!;
- [ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!;
- }
- // Used by SS14.Admin
- public interface IBanCommon<TUnban> where TUnban : IUnbanCommon
- {
- int Id { get; set; }
- Guid? PlayerUserId { get; set; }
- NpgsqlInet? Address { get; set; }
- TypedHwid? HWId { get; set; }
- DateTime BanTime { get; set; }
- DateTime? ExpirationTime { get; set; }
- string Reason { get; set; }
- NoteSeverity Severity { get; set; }
- Guid? BanningAdmin { get; set; }
- TUnban? Unban { get; set; }
- }
- // Used by SS14.Admin
- public interface IUnbanCommon
- {
- int Id { get; set; }
- int BanId { get; set; }
- Guid? UnbanningAdmin { get; set; }
- DateTime UnbanTime { get; set; }
- }
- /// <summary>
- /// Flags for use with <see cref="ServerBanExemption"/>.
- /// </summary>
- [Flags]
- public enum ServerBanExemptFlags
- {
- // @formatter:off
- None = 0,
- /// <summary>
- /// Ban is a datacenter range, connections usually imply usage of a VPN service.
- /// </summary>
- Datacenter = 1 << 0,
- /// <summary>
- /// Ban only matches the IP.
- /// </summary>
- /// <remarks>
- /// Intended use is for users with shared connections. This should not be used as an alternative to <see cref="Datacenter"/>.
- /// </remarks>
- IP = 1 << 1,
- /// <summary>
- /// Ban is an IP range that is only applied for first time joins.
- /// </summary>
- /// <remarks>
- /// Intended for use with residential IP ranges that are often used maliciously.
- /// </remarks>
- BlacklistedRange = 1 << 2,
- /// <summary>
- /// Represents having all possible exemption flags.
- /// </summary>
- All = int.MaxValue,
- // @formatter:on
- }
- /// <summary>
- /// A ban from playing on the server.
- /// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server.
- /// </summary>
- /// <remarks>
- /// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing).
- /// </remarks>
- [Table("server_ban"), Index(nameof(PlayerUserId))]
- public class ServerBan : IBanCommon<ServerUnban>
- {
- public int Id { get; set; }
- [ForeignKey("Round")]
- public int? RoundId { get; set; }
- public Round? Round { get; set; }
- /// <summary>
- /// The user ID of the banned player.
- /// </summary>
- public Guid? PlayerUserId { get; set; }
- [Required] public TimeSpan PlaytimeAtNote { get; set; }
- /// <summary>
- /// CIDR IP address range of the ban. The whole range can match the ban.
- /// </summary>
- public NpgsqlInet? Address { get; set; }
- /// <summary>
- /// Hardware ID of the banned player.
- /// </summary>
- public TypedHwid? HWId { get; set; }
- /// <summary>
- /// The time when the ban was applied by an administrator.
- /// </summary>
- public DateTime BanTime { get; set; }
- /// <summary>
- /// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
- /// </summary>
- public DateTime? ExpirationTime { get; set; }
- /// <summary>
- /// The administrator-stated reason for applying the ban.
- /// </summary>
- public string Reason { get; set; } = null!;
- /// <summary>
- /// The severity of the incident
- /// </summary>
- public NoteSeverity Severity { get; set; }
- /// <summary>
- /// User ID of the admin that applied the ban.
- /// </summary>
- [ForeignKey("CreatedBy")]
- public Guid? BanningAdmin { get; set; }
- public Player? CreatedBy { get; set; }
- /// <summary>
- /// User ID of the admin that last edited the note
- /// </summary>
- [ForeignKey("LastEditedBy")]
- public Guid? LastEditedById { get; set; }
- public Player? LastEditedBy { get; set; }
- /// <summary>
- /// When the ban was last edited
- /// </summary>
- public DateTime? LastEditedAt { get; set; }
- /// <summary>
- /// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
- /// </summary>
- public ServerBanExemptFlags ExemptFlags { get; set; }
- /// <summary>
- /// If present, an administrator has manually repealed this ban.
- /// </summary>
- public ServerUnban? Unban { get; set; }
- /// <summary>
- /// Whether this ban should be automatically deleted from the database when it expires.
- /// </summary>
- /// <remarks>
- /// This isn't done automatically by the game,
- /// you will need to set up something like a cron job to clear this from your database,
- /// using a command like this:
- /// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()"
- /// </remarks>
- public bool AutoDelete { get; set; }
- /// <summary>
- /// Whether to display this ban in the admin remarks (notes) panel
- /// </summary>
- public bool Hidden { get; set; }
- public List<ServerBanHit> BanHits { get; set; } = null!;
- }
- /// <summary>
- /// An explicit repeal of a <see cref="ServerBan"/> by an administrator.
- /// Having an entry for a ban neutralizes it.
- /// </summary>
- [Table("server_unban")]
- public class ServerUnban : IUnbanCommon
- {
- [Column("unban_id")] public int Id { get; set; }
- /// <summary>
- /// The ID of ban that is being repealed.
- /// </summary>
- public int BanId { get; set; }
- /// <summary>
- /// The ban that is being repealed.
- /// </summary>
- public ServerBan Ban { get; set; } = null!;
- /// <summary>
- /// The admin that repealed the ban.
- /// </summary>
- public Guid? UnbanningAdmin { get; set; }
- /// <summary>
- /// The time the ban repealed.
- /// </summary>
- public DateTime UnbanTime { get; set; }
- }
- /// <summary>
- /// An exemption for a specific user to a certain type of <see cref="ServerBan"/>.
- /// </summary>
- /// <example>
- /// Certain players may need to be exempted from VPN bans due to issues with their ISP.
- /// We would tag all VPN bans with <see cref="ServerBanExemptFlags.Datacenter"/>,
- /// and then add an exemption for these players to this table with the same flag.
- /// They will only be exempted from VPN bans, other bans (if they manage to get any) will still apply.
- /// </example>
- [Table("server_ban_exemption")]
- public sealed class ServerBanExemption
- {
- /// <summary>
- /// The UserID of the exempted player.
- /// </summary>
- [Key]
- public Guid UserId { get; set; }
- /// <summary>
- /// The ban flags to exempt this player from.
- /// If any bit overlaps <see cref="ServerBan.ExemptFlags"/>, the ban is ignored.
- /// </summary>
- public ServerBanExemptFlags Flags { get; set; }
- }
- [Table("connection_log")]
- public class ConnectionLog
- {
- public int Id { get; set; }
- public Guid UserId { get; set; }
- public string UserName { get; set; } = null!;
- public DateTime Time { get; set; }
- public IPAddress Address { get; set; } = null!;
- public TypedHwid? HWId { get; set; }
- public ConnectionDenyReason? Denied { get; set; }
- /// <summary>
- /// ID of the <see cref="Server"/> that the connection was attempted to.
- /// </summary>
- /// <remarks>
- /// <para>
- /// The default value of this column is set to <c>0</c>, which is the ID of the "<c>unknown</c>" server.
- /// This is intended for old entries (that didn't track this) and if the server name isn't configured.
- /// </para>
- /// </remarks>
- public int ServerId { get; set; }
- public List<ServerBanHit> BanHits { get; set; } = null!;
- public Server Server { get; set; } = null!;
- public float Trust { get; set; }
- }
- public enum ConnectionDenyReason : byte
- {
- Ban = 0,
- Whitelist = 1,
- Full = 2,
- Panic = 3,
- /*
- * If baby jail is removed, please reserve this value for as long as can reasonably be done to prevent causing ambiguity in connection denial reasons.
- * Reservation by commenting out the value is likely sufficient for this purpose, but may impact projects which depend on SS14 like SS14.Admin.
- *
- * Edit: It has
- */
- BabyJail = 4,
- /// Results from rejected connections with external API checking tools
- IPChecks = 5,
- /// Results from rejected connections who are authenticated but have no modern hwid associated with them.
- NoHwid = 6
- }
- public class ServerBanHit
- {
- public int Id { get; set; }
- public int BanId { get; set; }
- public int ConnectionId { get; set; }
- public ServerBan Ban { get; set; } = null!;
- public ConnectionLog Connection { get; set; } = null!;
- }
- [Table("server_role_ban"), Index(nameof(PlayerUserId))]
- public sealed class ServerRoleBan : IBanCommon<ServerRoleUnban>
- {
- public int Id { get; set; }
- public int? RoundId { get; set; }
- public Round? Round { get; set; }
- public Guid? PlayerUserId { get; set; }
- [Required] public TimeSpan PlaytimeAtNote { get; set; }
- public NpgsqlInet? Address { get; set; }
- public TypedHwid? HWId { get; set; }
- public DateTime BanTime { get; set; }
- public DateTime? ExpirationTime { get; set; }
- public string Reason { get; set; } = null!;
- public NoteSeverity Severity { get; set; }
- [ForeignKey("CreatedBy")] public Guid? BanningAdmin { get; set; }
- public Player? CreatedBy { get; set; }
- [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; }
- public Player? LastEditedBy { get; set; }
- public DateTime? LastEditedAt { get; set; }
- public ServerRoleUnban? Unban { get; set; }
- public bool Hidden { get; set; }
- public string RoleId { get; set; } = null!;
- }
- [Table("server_role_unban")]
- public sealed class ServerRoleUnban : IUnbanCommon
- {
- [Column("role_unban_id")] public int Id { get; set; }
- public int BanId { get; set; }
- public ServerRoleBan Ban { get; set; } = null!;
- public Guid? UnbanningAdmin { get; set; }
- public DateTime UnbanTime { get; set; }
- }
- [Table("play_time")]
- public sealed class PlayTime
- {
- [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; set; }
- [Required, ForeignKey("player")]
- public Guid PlayerId { get; set; }
- public string Tracker { get; set; } = null!;
- public TimeSpan TimeSpent { get; set; }
- }
- [Table("uploaded_resource_log")]
- public sealed class UploadedResourceLog
- {
- [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int Id { get; set; }
- public DateTime Date { get; set; }
- public Guid UserId { get; set; }
- public string Path { get; set; } = string.Empty;
- public byte[] Data { get; set; } = default!;
- }
- // Note: this interface isn't used by the game, but it *is* used by SS14.Admin.
- // Don't remove! Or face the consequences!
- public interface IAdminRemarksCommon
- {
- public int Id { get; }
- public int? RoundId { get; }
- public Round? Round { get; }
- public Guid? PlayerUserId { get; }
- public Player? Player { get; }
- public TimeSpan PlaytimeAtNote { get; }
- public string Message { get; }
- public Player? CreatedBy { get; }
- public DateTime CreatedAt { get; }
- public Player? LastEditedBy { get; }
- public DateTime? LastEditedAt { get; }
- public DateTime? ExpirationTime { get; }
- public bool Deleted { get; }
- }
- [Index(nameof(PlayerUserId))]
- public class AdminNote : IAdminRemarksCommon
- {
- [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
- [ForeignKey("Round")] public int? RoundId { get; set; }
- public Round? Round { get; set; }
- [ForeignKey("Player")] public Guid? PlayerUserId { get; set; }
- public Player? Player { get; set; }
- [Required] public TimeSpan PlaytimeAtNote { get; set; }
- [Required, MaxLength(4096)] public string Message { get; set; } = string.Empty;
- [Required] public NoteSeverity Severity { get; set; }
- [ForeignKey("CreatedBy")] public Guid? CreatedById { get; set; }
- public Player? CreatedBy { get; set; }
- [Required] public DateTime CreatedAt { get; set; }
- [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; }
- public Player? LastEditedBy { get; set; }
- [Required] public DateTime? LastEditedAt { get; set; }
- public DateTime? ExpirationTime { get; set; }
- public bool Deleted { get; set; }
- [ForeignKey("DeletedBy")] public Guid? DeletedById { get; set; }
- public Player? DeletedBy { get; set; }
- public DateTime? DeletedAt { get; set; }
- public bool Secret { get; set; }
- }
- [Index(nameof(PlayerUserId))]
- public class AdminWatchlist : IAdminRemarksCommon
- {
- [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
- [ForeignKey("Round")] public int? RoundId { get; set; }
- public Round? Round { get; set; }
- [ForeignKey("Player")] public Guid? PlayerUserId { get; set; }
- public Player? Player { get; set; }
- [Required] public TimeSpan PlaytimeAtNote { get; set; }
- [Required, MaxLength(4096)] public string Message { get; set; } = string.Empty;
- [ForeignKey("CreatedBy")] public Guid? CreatedById { get; set; }
- public Player? CreatedBy { get; set; }
- [Required] public DateTime CreatedAt { get; set; }
- [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; }
- public Player? LastEditedBy { get; set; }
- [Required] public DateTime? LastEditedAt { get; set; }
- public DateTime? ExpirationTime { get; set; }
- public bool Deleted { get; set; }
- [ForeignKey("DeletedBy")] public Guid? DeletedById { get; set; }
- public Player? DeletedBy { get; set; }
- public DateTime? DeletedAt { get; set; }
- }
- [Index(nameof(PlayerUserId))]
- public class AdminMessage : IAdminRemarksCommon
- {
- [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
- [ForeignKey("Round")] public int? RoundId { get; set; }
- public Round? Round { get; set; }
- [ForeignKey("Player")]
- public Guid? PlayerUserId { get; set; }
- public Player? Player { get; set; }
- [Required] public TimeSpan PlaytimeAtNote { get; set; }
- [Required, MaxLength(4096)] public string Message { get; set; } = string.Empty;
- [ForeignKey("CreatedBy")] public Guid? CreatedById { get; set; }
- public Player? CreatedBy { get; set; }
- [Required] public DateTime CreatedAt { get; set; }
- [ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; }
- public Player? LastEditedBy { get; set; }
- public DateTime? LastEditedAt { get; set; }
- public DateTime? ExpirationTime { get; set; }
- public bool Deleted { get; set; }
- [ForeignKey("DeletedBy")] public Guid? DeletedById { get; set; }
- public Player? DeletedBy { get; set; }
- public DateTime? DeletedAt { get; set; }
- /// <summary>
- /// Whether the message has been seen at least once by the player.
- /// </summary>
- public bool Seen { get; set; }
- /// <summary>
- /// Whether the message has been dismissed permanently by the player.
- /// </summary>
- public bool Dismissed { get; set; }
- }
- [PrimaryKey(nameof(PlayerUserId), nameof(RoleId))]
- public class RoleWhitelist
- {
- [Required, ForeignKey("Player")]
- public Guid PlayerUserId { get; set; }
- public Player Player { get; set; } = default!;
- [Required]
- public string RoleId { get; set; } = default!;
- }
- /// <summary>
- /// Defines a template that admins can use to quickly fill out ban information.
- /// </summary>
- /// <remarks>
- /// <para>
- /// This information is not currently used by the game itself, but it is used by SS14.Admin.
- /// </para>
- /// </remarks>
- public sealed class BanTemplate
- {
- public int Id { get; set; }
- /// <summary>
- /// Title of the ban template. This is purely for reference by admins and not copied into the ban.
- /// </summary>
- public required string Title { get; set; }
- /// <summary>
- /// How long the ban should last. 0 for permanent.
- /// </summary>
- public TimeSpan Length { get; set; }
- /// <summary>
- /// The reason for the ban.
- /// </summary>
- /// <seealso cref="ServerBan.Reason"/>
- public string Reason { get; set; } = "";
- /// <summary>
- /// Exemptions granted to the ban.
- /// </summary>
- /// <seealso cref="ServerBan.ExemptFlags"/>
- public ServerBanExemptFlags ExemptFlags { get; set; }
- /// <summary>
- /// Severity of the ban
- /// </summary>
- /// <seealso cref="ServerBan.Severity"/>
- public NoteSeverity Severity { get; set; }
- /// <summary>
- /// Ban will be automatically deleted once expired.
- /// </summary>
- /// <seealso cref="ServerBan.AutoDelete"/>
- public bool AutoDelete { get; set; }
- /// <summary>
- /// Ban is not visible to players in the remarks menu.
- /// </summary>
- /// <seealso cref="ServerBan.Hidden"/>
- public bool Hidden { get; set; }
- }
- /// <summary>
- /// A hardware ID value together with its <see cref="HwidType"/>.
- /// </summary>
- /// <seealso cref="ImmutableTypedHwid"/>
- [Owned]
- public sealed class TypedHwid
- {
- public byte[] Hwid { get; set; } = default!;
- public HwidType Type { get; set; }
- [return: NotNullIfNotNull(nameof(immutable))]
- public static implicit operator TypedHwid?(ImmutableTypedHwid? immutable)
- {
- if (immutable == null)
- return null;
- return new TypedHwid
- {
- Hwid = immutable.Hwid.ToArray(),
- Type = immutable.Type,
- };
- }
- [return: NotNullIfNotNull(nameof(hwid))]
- public static implicit operator ImmutableTypedHwid?(TypedHwid? hwid)
- {
- if (hwid == null)
- return null;
- return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type);
- }
- }
- /// <summary>
- /// Cache for the IPIntel system
- /// </summary>
- public class IPIntelCache
- {
- public int Id { get; set; }
- /// <summary>
- /// The IP address (duh). This is made unique manually for psql cause of ef core bug.
- /// </summary>
- public IPAddress Address { get; set; } = null!;
- /// <summary>
- /// Date this record was added. Used to check if our cache is out of date.
- /// </summary>
- public DateTime Time { get; set; }
- /// <summary>
- /// The score IPIntel returned
- /// </summary>
- public float Score { get; set; }
- }
- }
|