using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using Content.Shared.Database; using Microsoft.EntityFrameworkCore; using NpgsqlTypes; // ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength namespace Content.Server.Database; // // Contains model definitions primarily related to bans. // internal static class ModelBan { public static void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasOne(b => b.CreatedBy) .WithMany(pl => pl.AdminServerBansCreated) .HasForeignKey(b => b.BanningAdmin) .HasPrincipalKey(pl => pl.UserId) .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity() .HasOne(b => b.LastEditedBy) .WithMany(pl => pl.AdminServerBansLastEdited) .HasForeignKey(b => b.LastEditedById) .HasPrincipalKey(pl => pl.UserId) .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity() .HasIndex(bp => new { bp.UserId, bp.BanId }) .IsUnique(); modelBuilder.Entity() .OwnsOne(bp => bp.HWId) .Property(hwid => hwid.Hwid) .HasColumnName("hwid"); modelBuilder.Entity() .HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId }) .IsUnique(); modelBuilder.Entity() .HasIndex(bp => new { bp.RoundId, bp.BanId }) .IsUnique(); // Following indices have to be made manually by migration, due to limitations in EF Core: // https://github.com/dotnet/efcore/issues/11336 // https://github.com/npgsql/efcore.pg/issues/2567 // modelBuilder.Entity() // .HasIndex(bp => new { bp.Address, bp.BanId }) // .IsUnique(); // modelBuilder.Entity() // .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid }) // .IsUnique(); // (postgres only) // modelBuilder.Entity() // .HasIndex(ba => ba.Address) // .IncludeProperties(ba => ba.BanId) // .IsUnique() // .HasMethod("gist") // .HasOperators("inet_ops"); modelBuilder.Entity() .ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0")); } } /// /// Specifies a ban of some kind. /// /// /// /// Bans come in two types: and , /// distinguished with . /// /// /// Bans have one or more "matching data", these being , , /// and entities. If a player's connection info matches any of these, /// the ban's effects will apply to that player. /// /// /// Bans can be set to expire after a certain point in time, or be permanent. They can also be removed manually /// ("unbanned") by an admin, which is stored as an entity existing for this ban. /// /// public sealed class Ban { public int Id { get; set; } /// /// Whether this is a role or server ban. /// public required BanType Type { get; set; } public TimeSpan PlaytimeAtNote { get; set; } /// /// The time when the ban was applied by an administrator. /// public DateTime BanTime { get; set; } /// /// The time the ban will expire. If null, the ban is permanent and will not expire naturally. /// public DateTime? ExpirationTime { get; set; } /// /// The administrator-stated reason for applying the ban. /// public string Reason { get; set; } = null!; /// /// The severity of the incident /// public NoteSeverity Severity { get; set; } /// /// User ID of the admin that initially applied the ban. /// [ForeignKey(nameof(CreatedBy))] public Guid? BanningAdmin { get; set; } public Player? CreatedBy { get; set; } /// /// User ID of the admin that last edited the note /// [ForeignKey(nameof(LastEditedBy))] public Guid? LastEditedById { get; set; } public Player? LastEditedBy { get; set; } public DateTime? LastEditedAt { get; set; } /// /// Optional flags that allow adding exemptions to the ban via . /// public ServerBanExemptFlags ExemptFlags { get; set; } /// /// Whether this ban should be automatically deleted from the database when it expires. /// /// /// 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()" /// public bool AutoDelete { get; set; } /// /// Whether to display this ban in the admin remarks (notes) panel /// public bool Hidden { get; set; } /// /// If present, an administrator has manually repealed this ban. /// public Unban? Unban { get; set; } public List? Rounds { get; set; } public List? Players { get; set; } public List? Addresses { get; set; } public List? Hwids { get; set; } public List? Roles { get; set; } public List? BanHits { get; set; } } /// /// Base type for entities that specify ban matching data. /// public interface IBanSelector { int BanId { get; } Ban? Ban { get; } } /// /// Indicates that a ban was related to a round (e.g. placed on that round). /// public sealed class BanRound { public int Id { get; set; } /// /// The ID of the ban to which this round was relevant. /// [ForeignKey(nameof(Ban))] public int BanId { get; set; } public Ban? Ban { get; set; } /// /// The ID of the round to which this ban was relevant to. /// [ForeignKey(nameof(Round))] public int RoundId { get; set; } public Round? Round { get; set; } } /// /// Specifies a player that a matches. /// public sealed class BanPlayer : IBanSelector { public int Id { get; set; } /// /// The user ID of the banned player. /// public Guid UserId { get; set; } /// /// The ID of the ban to which this applies. /// [ForeignKey(nameof(Ban))] public int BanId { get; set; } public Ban? Ban { get; set; } } /// /// Specifies an IP address range that a matches. /// public sealed class BanAddress : IBanSelector { public int Id { get; set; } /// /// The address range being matched. /// public required NpgsqlInet Address { get; set; } /// /// The ID of the ban to which this applies. /// [ForeignKey(nameof(Ban))] public int BanId { get; set; } public Ban? Ban { get; set; } } /// /// Specifies a HWID that a matches. /// public sealed class BanHwid : IBanSelector { public int Id { get; set; } /// /// The HWID being matched. /// public required TypedHwid HWId { get; set; } /// /// The ID of the ban to which this applies. /// [ForeignKey(nameof(Ban))] public int BanId { get; set; } public Ban? Ban { get; set; } } /// /// A single role banned among a greater role ban record. /// /// /// s of type should have one or more s /// to store which roles are actually banned. /// It is invalid for bans to have entities. /// public sealed class BanRole { public int Id { get; set; } /// /// What type of role is being banned. For example Job or Antag. /// public required string RoleType { get; set; } /// /// The ID of the role being banned. This is probably something like a prototype. /// public required string RoleId { get; set; } /// /// The ID of the ban to which this applies. /// [ForeignKey(nameof(Ban))] public int BanId { get; set; } public Ban? Ban { get; set; } } /// /// An explicit repeal of a by an administrator. /// Having an entry for a ban neutralizes it. /// public sealed class Unban { public int Id { get; set; } /// /// The ID of ban that is being repealed. /// [ForeignKey(nameof(Ban))] public int BanId { get; set; } /// /// The ban that is being repealed. /// public Ban? Ban { get; set; } /// /// The admin that repealed the ban. /// public Guid? UnbanningAdmin { get; set; } /// /// The time the ban was repealed. /// public DateTime UnbanTime { get; set; } }