| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850 |
- using System.Collections.Immutable;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Net;
- using System.Runtime.CompilerServices;
- using System.Text.Json;
- using System.Threading;
- using System.Threading.Tasks;
- using Content.Server.Administration.Logs;
- using Content.Server.Administration.Managers;
- using Content.Shared.Administration.Logs;
- using Content.Shared.Database;
- using Content.Shared.Humanoid;
- using Content.Shared.Humanoid.Markings;
- using Content.Shared.Preferences;
- using Content.Shared.Preferences.Loadouts;
- using Content.Shared.Roles;
- using Content.Shared.Traits;
- using Microsoft.EntityFrameworkCore;
- using Robust.Shared.Enums;
- using Robust.Shared.Network;
- using Robust.Shared.Prototypes;
- using Robust.Shared.Utility;
- namespace Content.Server.Database
- {
- public abstract class ServerDbBase
- {
- private readonly ISawmill _opsLog;
- public event Action<DatabaseNotification>? OnNotificationReceived;
- /// <param name="opsLog">Sawmill to trace log database operations to.</param>
- public ServerDbBase(ISawmill opsLog)
- {
- _opsLog = opsLog;
- }
- #region Preferences
- public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(
- NetUserId userId,
- CancellationToken cancel = default)
- {
- await using var db = await GetDb(cancel);
- var prefs = await db.DbContext
- .Preference
- .Include(p => p.Profiles).ThenInclude(h => h.Jobs)
- .Include(p => p.Profiles).ThenInclude(h => h.Antags)
- .Include(p => p.Profiles).ThenInclude(h => h.Traits)
- .Include(p => p.Profiles)
- .ThenInclude(h => h.Loadouts)
- .ThenInclude(l => l.Groups)
- .ThenInclude(group => group.Loadouts)
- .AsSplitQuery()
- .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
- if (prefs is null)
- return null;
- var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1;
- var profiles = new Dictionary<int, ICharacterProfile>(maxSlot);
- foreach (var profile in prefs.Profiles)
- {
- profiles[profile.Slot] = ConvertProfiles(profile);
- }
- return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor));
- }
- public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
- {
- await using var db = await GetDb();
- await SetSelectedCharacterSlotAsync(userId, index, db.DbContext);
- await db.DbContext.SaveChangesAsync();
- }
- public async Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
- {
- await using var db = await GetDb();
- if (profile is null)
- {
- await DeleteCharacterSlot(db.DbContext, userId, slot);
- await db.DbContext.SaveChangesAsync();
- return;
- }
- if (profile is not HumanoidCharacterProfile humanoid)
- {
- // TODO: Handle other ICharacterProfile implementations properly
- throw new NotImplementedException();
- }
- var oldProfile = db.DbContext.Profile
- .Include(p => p.Preference)
- .Where(p => p.Preference.UserId == userId.UserId)
- .Include(p => p.Jobs)
- .Include(p => p.Antags)
- .Include(p => p.Traits)
- .Include(p => p.Loadouts)
- .ThenInclude(l => l.Groups)
- .ThenInclude(group => group.Loadouts)
- .AsSplitQuery()
- .SingleOrDefault(h => h.Slot == slot);
- var newProfile = ConvertProfiles(humanoid, slot, oldProfile);
- if (oldProfile == null)
- {
- var prefs = await db.DbContext
- .Preference
- .Include(p => p.Profiles)
- .SingleAsync(p => p.UserId == userId.UserId);
- prefs.Profiles.Add(newProfile);
- }
- await db.DbContext.SaveChangesAsync();
- }
- private static async Task DeleteCharacterSlot(ServerDbContext db, NetUserId userId, int slot)
- {
- var profile = await db.Profile.Include(p => p.Preference)
- .Where(p => p.Preference.UserId == userId.UserId && p.Slot == slot)
- .SingleOrDefaultAsync();
- if (profile == null)
- {
- return;
- }
- db.Profile.Remove(profile);
- }
- public async Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
- {
- await using var db = await GetDb();
- var profile = ConvertProfiles((HumanoidCharacterProfile) defaultProfile, 0);
- var prefs = new Preference
- {
- UserId = userId.UserId,
- SelectedCharacterSlot = 0,
- AdminOOCColor = Color.Red.ToHex()
- };
- prefs.Profiles.Add(profile);
- db.DbContext.Preference.Add(prefs);
- await db.DbContext.SaveChangesAsync();
- return new PlayerPreferences(new[] {new KeyValuePair<int, ICharacterProfile>(0, defaultProfile)}, 0, Color.FromHex(prefs.AdminOOCColor));
- }
- public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
- {
- await using var db = await GetDb();
- await DeleteCharacterSlot(db.DbContext, userId, deleteSlot);
- await SetSelectedCharacterSlotAsync(userId, newSlot, db.DbContext);
- await db.DbContext.SaveChangesAsync();
- }
- public async Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
- {
- await using var db = await GetDb();
- var prefs = await db.DbContext
- .Preference
- .Include(p => p.Profiles)
- .SingleAsync(p => p.UserId == userId.UserId);
- prefs.AdminOOCColor = color.ToHex();
- await db.DbContext.SaveChangesAsync();
- }
- private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int newSlot, ServerDbContext db)
- {
- var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId);
- prefs.SelectedCharacterSlot = newSlot;
- }
- private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
- {
- var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
- var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
- var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
- var sex = Sex.Male;
- if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
- sex = sexVal;
- var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
- var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
- if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
- gender = genderVal;
- // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
- var markingsRaw = profile.Markings?.Deserialize<List<string>>();
- List<Marking> markings = new();
- if (markingsRaw != null)
- {
- foreach (var marking in markingsRaw)
- {
- var parsed = Marking.ParseFromDbString(marking);
- if (parsed is null) continue;
- markings.Add(parsed);
- }
- }
- var loadouts = new Dictionary<string, RoleLoadout>();
- foreach (var role in profile.Loadouts)
- {
- var loadout = new RoleLoadout(role.RoleName)
- {
- EntityName = role.EntityName,
- };
- foreach (var group in role.Groups)
- {
- var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
- foreach (var profLoadout in group.Loadouts)
- {
- groupLoadouts.Add(new Loadout()
- {
- Prototype = profLoadout.LoadoutName,
- });
- }
- }
- loadouts[role.RoleName] = loadout;
- }
- return new HumanoidCharacterProfile(
- profile.CharacterName,
- profile.FlavorText,
- profile.Species,
- profile.Age,
- sex,
- gender,
- new HumanoidCharacterAppearance
- (
- profile.HairName,
- Color.FromHex(profile.HairColor),
- profile.FacialHairName,
- Color.FromHex(profile.FacialHairColor),
- Color.FromHex(profile.EyeColor),
- Color.FromHex(profile.SkinColor),
- markings
- ),
- spawnPriority,
- jobs,
- (PreferenceUnavailableMode) profile.PreferenceUnavailable,
- antags.ToHashSet(),
- traits.ToHashSet(),
- loadouts
- );
- }
- private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
- {
- profile ??= new Profile();
- var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
- List<string> markingStrings = new();
- foreach (var marking in appearance.Markings)
- {
- markingStrings.Add(marking.ToString());
- }
- var markings = JsonSerializer.SerializeToDocument(markingStrings);
- profile.CharacterName = humanoid.Name;
- profile.FlavorText = humanoid.FlavorText;
- profile.Species = humanoid.Species;
- profile.Age = humanoid.Age;
- profile.Sex = humanoid.Sex.ToString();
- profile.Gender = humanoid.Gender.ToString();
- profile.HairName = appearance.HairStyleId;
- profile.HairColor = appearance.HairColor.ToHex();
- profile.FacialHairName = appearance.FacialHairStyleId;
- profile.FacialHairColor = appearance.FacialHairColor.ToHex();
- profile.EyeColor = appearance.EyeColor.ToHex();
- profile.SkinColor = appearance.SkinColor.ToHex();
- profile.SpawnPriority = (int) humanoid.SpawnPriority;
- profile.Markings = markings;
- profile.Slot = slot;
- profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
- profile.Jobs.Clear();
- profile.Jobs.AddRange(
- humanoid.JobPriorities
- .Where(j => j.Value != JobPriority.Never)
- .Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
- );
- profile.Antags.Clear();
- profile.Antags.AddRange(
- humanoid.AntagPreferences
- .Select(a => new Antag {AntagName = a})
- );
- profile.Traits.Clear();
- profile.Traits.AddRange(
- humanoid.TraitPreferences
- .Select(t => new Trait {TraitName = t})
- );
- profile.Loadouts.Clear();
- foreach (var (role, loadouts) in humanoid.Loadouts)
- {
- var dz = new ProfileRoleLoadout()
- {
- RoleName = role,
- EntityName = loadouts.EntityName ?? string.Empty,
- };
- foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
- {
- var profileGroup = new ProfileLoadoutGroup()
- {
- GroupName = group,
- };
- foreach (var loadout in groupLoadouts)
- {
- profileGroup.Loadouts.Add(new ProfileLoadout()
- {
- LoadoutName = loadout.Prototype,
- });
- }
- dz.Groups.Add(profileGroup);
- }
- profile.Loadouts.Add(dz);
- }
- return profile;
- }
- #endregion
- #region User Ids
- public async Task<NetUserId?> GetAssignedUserIdAsync(string name)
- {
- await using var db = await GetDb();
- var assigned = await db.DbContext.AssignedUserId.SingleOrDefaultAsync(p => p.UserName == name);
- return assigned?.UserId is { } g ? new NetUserId(g) : default(NetUserId?);
- }
- public async Task AssignUserIdAsync(string name, NetUserId netUserId)
- {
- await using var db = await GetDb();
- db.DbContext.AssignedUserId.Add(new AssignedUserId
- {
- UserId = netUserId.UserId,
- UserName = name
- });
- await db.DbContext.SaveChangesAsync();
- }
- #endregion
- #region Bans
- /*
- * BAN STUFF
- */
- /// <summary>
- /// Looks up a ban by id.
- /// This will return a pardoned ban as well.
- /// </summary>
- /// <param name="id">The ban id to look for.</param>
- /// <returns>The ban with the given id or null if none exist.</returns>
- public abstract Task<ServerBanDef?> GetServerBanAsync(int id);
- /// <summary>
- /// Looks up an user's most recent received un-pardoned ban.
- /// This will NOT return a pardoned ban.
- /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
- /// </summary>
- /// <param name="address">The ip address of the user.</param>
- /// <param name="userId">The id of the user.</param>
- /// <param name="hwId">The legacy HWId of the user.</param>
- /// <param name="modernHWIds">The modern HWIDs of the user.</param>
- /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
- public abstract Task<ServerBanDef?> GetServerBanAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds);
- /// <summary>
- /// Looks up an user's ban history.
- /// This will return pardoned bans as well.
- /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
- /// </summary>
- /// <param name="address">The ip address of the user.</param>
- /// <param name="userId">The id of the user.</param>
- /// <param name="hwId">The legacy HWId of the user.</param>
- /// <param name="modernHWIds">The modern HWIDs of the user.</param>
- /// <param name="includeUnbanned">Include pardoned and expired bans.</param>
- /// <returns>The user's ban history.</returns>
- public abstract Task<List<ServerBanDef>> GetServerBansAsync(
- IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds,
- bool includeUnbanned);
- public abstract Task AddServerBanAsync(ServerBanDef serverBan);
- public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
- public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
- {
- await using var db = await GetDb();
- var ban = await db.DbContext.Ban.SingleOrDefaultAsync(b => b.Id == id);
- if (ban is null)
- return;
- ban.Severity = severity;
- ban.Reason = reason;
- ban.ExpirationTime = expiration?.UtcDateTime;
- ban.LastEditedById = editedBy;
- ban.LastEditedAt = editedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(
- DbGuard db,
- NetUserId? userId,
- CancellationToken cancel = default)
- {
- if (userId == null)
- return null;
- var exemption = await db.DbContext.BanExemption
- .SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId, cancellationToken: cancel);
- return exemption?.Flags;
- }
- public async Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
- {
- await using var db = await GetDb();
- if (flags == 0)
- {
- // Delete whatever is there.
- await db.DbContext.BanExemption.Where(u => u.UserId == userId.UserId).ExecuteDeleteAsync();
- return;
- }
- var exemption = await db.DbContext.BanExemption.SingleOrDefaultAsync(u => u.UserId == userId.UserId);
- if (exemption == null)
- {
- exemption = new ServerBanExemption
- {
- UserId = userId
- };
- db.DbContext.BanExemption.Add(exemption);
- }
- exemption.Flags = flags;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- var flags = await GetBanExemptionCore(db, userId, cancel);
- return flags ?? ServerBanExemptFlags.None;
- }
- #endregion
- #region Role Bans
- /*
- * ROLE BANS
- */
- /// <summary>
- /// Looks up a role ban by id.
- /// This will return a pardoned role ban as well.
- /// </summary>
- /// <param name="id">The role ban id to look for.</param>
- /// <returns>The role ban with the given id or null if none exist.</returns>
- public abstract Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
- /// <summary>
- /// Looks up an user's role ban history.
- /// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
- /// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
- /// </summary>
- /// <param name="address">The IP address of the user.</param>
- /// <param name="userId">The NetUserId of the user.</param>
- /// <param name="hwId">The Hardware Id of the user.</param>
- /// <param name="modernHWIds">The modern HWIDs of the user.</param>
- /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
- /// <returns>The user's role ban history.</returns>
- public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
- NetUserId? userId,
- ImmutableArray<byte>? hwId,
- ImmutableArray<ImmutableArray<byte>>? modernHWIds,
- bool includeUnbanned);
- public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
- public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
- public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
- {
- await using var db = await GetDb();
- var roleBanDetails = await db.DbContext.RoleBan
- .Where(b => b.Id == id)
- .Select(b => new { b.BanTime, b.PlayerUserId })
- .SingleOrDefaultAsync();
- if (roleBanDetails == default)
- return;
- await db.DbContext.RoleBan
- .Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId)
- .ExecuteUpdateAsync(setters => setters
- .SetProperty(b => b.Severity, severity)
- .SetProperty(b => b.Reason, reason)
- .SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null)
- .SetProperty(b => b.LastEditedById, editedBy)
- .SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime)
- );
- }
- #endregion
- #region Playtime
- public async Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- return await db.DbContext.PlayTime
- .Where(p => p.PlayerId == player)
- .ToListAsync(cancel);
- }
- public async Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
- {
- await using var db = await GetDb();
- // Ideally I would just be able to send a bunch of UPSERT commands, but EFCore is a pile of garbage.
- // So... In the interest of not making this take forever at high update counts...
- // Bulk-load play time objects for all players involved.
- // This allows us to semi-efficiently load all entities we need in a single DB query.
- // Then we can update & insert without further round-trips to the DB.
- var players = updates.Select(u => u.User.UserId).Distinct().ToArray();
- var dbTimes = (await db.DbContext.PlayTime
- .Where(p => players.Contains(p.PlayerId))
- .ToArrayAsync())
- .GroupBy(p => p.PlayerId)
- .ToDictionary(g => g.Key, g => g.ToDictionary(p => p.Tracker, p => p));
- foreach (var (user, tracker, time) in updates)
- {
- if (dbTimes.TryGetValue(user.UserId, out var userTimes)
- && userTimes.TryGetValue(tracker, out var ent))
- {
- // Already have a tracker in the database, update it.
- ent.TimeSpent = time;
- continue;
- }
- // No tracker, make a new one.
- var playTime = new PlayTime
- {
- Tracker = tracker,
- PlayerId = user.UserId,
- TimeSpent = time
- };
- db.DbContext.PlayTime.Add(playTime);
- }
- await db.DbContext.SaveChangesAsync();
- }
- #endregion
- #region Player Records
- /*
- * PLAYER RECORDS
- */
- public async Task UpdatePlayerRecord(
- NetUserId userId,
- string userName,
- IPAddress address,
- ImmutableTypedHwid? hwId)
- {
- await using var db = await GetDb();
- var record = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
- if (record == null)
- {
- db.DbContext.Player.Add(record = new Player
- {
- FirstSeenTime = DateTime.UtcNow,
- UserId = userId.UserId,
- });
- }
- record.LastSeenTime = DateTime.UtcNow;
- record.LastSeenAddress = address;
- record.LastSeenUserName = userName;
- record.LastSeenHWId = hwId;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel)
- {
- await using var db = await GetDb();
- // Sort by descending last seen time.
- // So if, due to account renames, we have two people with the same username in the DB,
- // the most recent one is picked.
- var record = await db.DbContext.Player
- .OrderByDescending(p => p.LastSeenTime)
- .FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel);
- return record == null ? null : MakePlayerRecord(record);
- }
- public async Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel)
- {
- await using var db = await GetDb();
- var record = await db.DbContext.Player
- .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
- return record == null ? null : MakePlayerRecord(record);
- }
- protected async Task<bool> PlayerRecordExists(DbGuard db, NetUserId userId)
- {
- return await db.DbContext.Player.AnyAsync(p => p.UserId == userId);
- }
- [return: NotNullIfNotNull(nameof(player))]
- protected PlayerRecord? MakePlayerRecord(Player? player)
- {
- if (player == null)
- return null;
- return new PlayerRecord(
- new NetUserId(player.UserId),
- new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)),
- player.LastSeenUserName,
- new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
- player.LastSeenAddress,
- player.LastSeenHWId);
- }
- #endregion
- #region Connection Logs
- /*
- * CONNECTION LOG
- */
- public abstract Task<int> AddConnectionLogAsync(NetUserId userId,
- string userName,
- IPAddress address,
- ImmutableTypedHwid? hwId,
- float trust,
- ConnectionDenyReason? denied,
- int serverId);
- public async Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
- {
- await using var db = await GetDb();
- foreach (var ban in bans)
- {
- db.DbContext.ServerBanHit.Add(new ServerBanHit
- {
- ConnectionId = connection, BanId = ban.Id!.Value
- });
- }
- await db.DbContext.SaveChangesAsync();
- }
- #endregion
- #region Admin Ranks
- /*
- * ADMIN RANKS
- */
- public async Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- return await db.DbContext.Admin
- .Include(p => p.Flags)
- .Include(p => p.AdminRank)
- .ThenInclude(p => p!.Flags)
- .AsSplitQuery() // tests fail because of a random warning if you dont have this!
- .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
- }
- public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])>
- GetAllAdminAndRanksAsync(CancellationToken cancel);
- public async Task<AdminRank?> GetAdminRankDataForAsync(int id, CancellationToken cancel = default)
- {
- await using var db = await GetDb(cancel);
- return await db.DbContext.AdminRank
- .Include(r => r.Flags)
- .SingleOrDefaultAsync(r => r.Id == id, cancel);
- }
- public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel);
- db.DbContext.Admin.Remove(admin);
- await db.DbContext.SaveChangesAsync(cancel);
- }
- public async Task AddAdminAsync(Admin admin, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- db.DbContext.Admin.Add(admin);
- await db.DbContext.SaveChangesAsync(cancel);
- }
- public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- var existing = await db.DbContext.Admin.Include(a => a.Flags).SingleAsync(a => a.UserId == admin.UserId, cancel);
- existing.Flags = admin.Flags;
- existing.Title = admin.Title;
- existing.AdminRankId = admin.AdminRankId;
- existing.Deadminned = admin.Deadminned;
- existing.Suspended = admin.Suspended;
- await db.DbContext.SaveChangesAsync(cancel);
- }
- public async Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- var adminRecord = db.DbContext.Admin.Where(a => a.UserId == userId);
- await adminRecord.ExecuteUpdateAsync(
- set => set.SetProperty(p => p.Deadminned, deadminned),
- cancellationToken: cancel);
- await db.DbContext.SaveChangesAsync(cancel);
- }
- public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel);
- db.DbContext.AdminRank.Remove(admin);
- await db.DbContext.SaveChangesAsync(cancel);
- }
- public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- db.DbContext.AdminRank.Add(rank);
- await db.DbContext.SaveChangesAsync(cancel);
- }
- public async Task<int> AddNewRound(Server server, params Guid[] playerIds)
- {
- await using var db = await GetDb();
- var players = await db.DbContext.Player
- .Where(player => playerIds.Contains(player.UserId))
- .ToListAsync();
- var round = new Round
- {
- StartDate = DateTime.UtcNow,
- Players = players,
- ServerId = server.Id
- };
- db.DbContext.Round.Add(round);
- await db.DbContext.SaveChangesAsync();
- return round.Id;
- }
- public async Task<Round> GetRound(int id)
- {
- await using var db = await GetDb();
- var round = await db.DbContext.Round
- .Include(round => round.Players)
- .SingleAsync(round => round.Id == id);
- return round;
- }
- public async Task AddRoundPlayers(int id, Guid[] playerIds)
- {
- await using var db = await GetDb();
- // ReSharper disable once SuggestVarOrType_Elsewhere
- Dictionary<Guid, int> players = await db.DbContext.Player
- .Where(player => playerIds.Contains(player.UserId))
- .ToDictionaryAsync(player => player.UserId, player => player.Id);
- foreach (var player in playerIds)
- {
- await db.DbContext.Database.ExecuteSqlAsync($"""
- INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}) ON CONFLICT DO NOTHING
- """);
- }
- await db.DbContext.SaveChangesAsync();
- }
- [return: NotNullIfNotNull(nameof(round))]
- protected RoundRecord? MakeRoundRecord(Round? round)
- {
- if (round == null)
- return null;
- return new RoundRecord(
- round.Id,
- NormalizeDatabaseTime(round.StartDate),
- MakeServerRecord(round.Server));
- }
- public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- var existing = await db.DbContext.AdminRank
- .Include(r => r.Flags)
- .SingleAsync(a => a.Id == rank.Id, cancel);
- existing.Flags = rank.Flags;
- existing.Name = rank.Name;
- await db.DbContext.SaveChangesAsync(cancel);
- }
- #endregion
- #region Admin Logs
- public async Task<(Server, bool existed)> AddOrGetServer(string serverName)
- {
- await using var db = await GetDb();
- var server = await db.DbContext.Server
- .Where(server => server.Name.Equals(serverName))
- .SingleOrDefaultAsync();
- if (server != default)
- return (server, true);
- server = new Server
- {
- Name = serverName
- };
- db.DbContext.Server.Add(server);
- await db.DbContext.SaveChangesAsync();
- return (server, false);
- }
- [return: NotNullIfNotNull(nameof(server))]
- protected ServerRecord? MakeServerRecord(Server? server)
- {
- if (server == null)
- return null;
- return new ServerRecord(server.Id, server.Name);
- }
- public async Task AddAdminLogs(List<AdminLog> logs)
- {
- const int maxRetryAttempts = 5;
- var initialRetryDelay = TimeSpan.FromSeconds(5);
- DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
- var attempt = 0;
- var retryDelay = initialRetryDelay;
- while (attempt < maxRetryAttempts)
- {
- try
- {
- await using var db = await GetDb();
- db.DbContext.AdminLog.AddRange(logs);
- await db.DbContext.SaveChangesAsync();
- _opsLog.Debug($"Successfully saved {logs.Count} admin logs.");
- break;
- }
- catch (Exception ex)
- {
- attempt += 1;
- _opsLog.Error($"Attempt {attempt} failed to save logs: {ex}");
- if (attempt >= maxRetryAttempts)
- {
- _opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
- return;
- }
- _opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
- await Task.Delay(retryDelay);
- retryDelay *= 2;
- }
- }
- }
- protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);
- private IQueryable<AdminLog> GetAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
- {
- // Save me from SQLite
- var query = StartAdminLogsQuery(db, filter);
- if (filter == null)
- {
- return query.OrderBy(log => log.Date);
- }
- if (filter.Round != null)
- {
- query = query.Where(log => log.RoundId == filter.Round);
- }
- if (filter.Types != null)
- {
- query = query.Where(log => filter.Types.Contains(log.Type));
- }
- if (filter.Impacts != null)
- {
- query = query.Where(log => filter.Impacts.Contains(log.Impact));
- }
- if (filter.Before != null)
- {
- query = query.Where(log => log.Date < filter.Before);
- }
- if (filter.After != null)
- {
- query = query.Where(log => log.Date > filter.After);
- }
- if (filter.IncludePlayers)
- {
- if (filter.AnyPlayers != null)
- {
- query = query.Where(log =>
- log.Players.Any(p => filter.AnyPlayers.Contains(p.PlayerUserId)) ||
- log.Players.Count == 0 && filter.IncludeNonPlayers);
- }
- if (filter.AllPlayers != null)
- {
- query = query.Where(log =>
- log.Players.All(p => filter.AllPlayers.Contains(p.PlayerUserId)) ||
- log.Players.Count == 0 && filter.IncludeNonPlayers);
- }
- }
- else
- {
- query = query.Where(log => log.Players.Count == 0);
- }
- if (filter.LastLogId != null)
- {
- query = filter.DateOrder switch
- {
- DateOrder.Ascending => query.Where(log => log.Id > filter.LastLogId),
- DateOrder.Descending => query.Where(log => log.Id < filter.LastLogId),
- _ => throw new ArgumentOutOfRangeException(nameof(filter),
- $"Unknown {nameof(DateOrder)} value {filter.DateOrder}")
- };
- }
- query = filter.DateOrder switch
- {
- DateOrder.Ascending => query.OrderBy(log => log.Date),
- DateOrder.Descending => query.OrderByDescending(log => log.Date),
- _ => throw new ArgumentOutOfRangeException(nameof(filter),
- $"Unknown {nameof(DateOrder)} value {filter.DateOrder}")
- };
- const int hardLogLimit = 500_000;
- if (filter.Limit != null)
- {
- query = query.Take(Math.Min(filter.Limit.Value, hardLogLimit));
- }
- else
- {
- query = query.Take(hardLogLimit);
- }
- return query;
- }
- public async IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
- {
- await using var db = await GetDb();
- var query = GetAdminLogsQuery(db.DbContext, filter);
- await foreach (var log in query.Select(log => log.Message).AsAsyncEnumerable())
- {
- yield return log;
- }
- }
- public async IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
- {
- await using var db = await GetDb();
- var query = GetAdminLogsQuery(db.DbContext, filter);
- query = query.Include(log => log.Players);
- await foreach (var log in query.AsAsyncEnumerable())
- {
- var players = new Guid[log.Players.Count];
- for (var i = 0; i < log.Players.Count; i++)
- {
- players[i] = log.Players[i].PlayerUserId;
- }
- yield return new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.Message, players);
- }
- }
- public async IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
- {
- await using var db = await GetDb();
- var query = GetAdminLogsQuery(db.DbContext, filter);
- await foreach (var json in query.Select(log => log.Json).AsAsyncEnumerable())
- {
- yield return json;
- }
- }
- public async Task<int> CountAdminLogs(int round)
- {
- await using var db = await GetDb();
- return await db.DbContext.AdminLog.CountAsync(log => log.RoundId == round);
- }
- #endregion
- #region Whitelist
- public async Task<bool> GetWhitelistStatusAsync(NetUserId player)
- {
- await using var db = await GetDb();
- return await db.DbContext.Whitelist.AnyAsync(w => w.UserId == player);
- }
- public async Task AddToWhitelistAsync(NetUserId player)
- {
- await using var db = await GetDb();
- db.DbContext.Whitelist.Add(new Whitelist { UserId = player });
- await db.DbContext.SaveChangesAsync();
- }
- public async Task RemoveFromWhitelistAsync(NetUserId player)
- {
- await using var db = await GetDb();
- var entry = await db.DbContext.Whitelist.SingleAsync(w => w.UserId == player);
- db.DbContext.Whitelist.Remove(entry);
- await db.DbContext.SaveChangesAsync();
- }
- public async Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
- {
- await using var db = await GetDb();
- return NormalizeDatabaseTime(await db.DbContext.Player
- .Where(dbPlayer => dbPlayer.UserId == player)
- .Select(dbPlayer => dbPlayer.LastReadRules)
- .SingleOrDefaultAsync());
- }
- public async Task SetLastReadRules(NetUserId player, DateTimeOffset? date)
- {
- await using var db = await GetDb();
- var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player).SingleOrDefaultAsync();
- if (dbPlayer == null)
- {
- return;
- }
- dbPlayer.LastReadRules = date?.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task<bool> GetBlacklistStatusAsync(NetUserId player)
- {
- await using var db = await GetDb();
- return await db.DbContext.Blacklist.AnyAsync(w => w.UserId == player);
- }
- public async Task AddToBlacklistAsync(NetUserId player)
- {
- await using var db = await GetDb();
- db.DbContext.Blacklist.Add(new Blacklist() { UserId = player });
- await db.DbContext.SaveChangesAsync();
- }
- public async Task RemoveFromBlacklistAsync(NetUserId player)
- {
- await using var db = await GetDb();
- var entry = await db.DbContext.Blacklist.SingleAsync(w => w.UserId == player);
- db.DbContext.Blacklist.Remove(entry);
- await db.DbContext.SaveChangesAsync();
- }
- #endregion
- #region Uploaded Resources Logs
- public async Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
- {
- await using var db = await GetDb();
- db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date.UtcDateTime, Path = path, Data = data });
- await db.DbContext.SaveChangesAsync();
- }
- public async Task PurgeUploadedResourceLogAsync(int days)
- {
- await using var db = await GetDb();
- var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(days));
- await foreach (var log in db.DbContext.UploadedResourceLog
- .Where(l => date > l.Date)
- .AsAsyncEnumerable())
- {
- db.DbContext.UploadedResourceLog.Remove(log);
- }
- await db.DbContext.SaveChangesAsync();
- }
- #endregion
- #region Admin Notes
- public virtual async Task<int> AddAdminNote(AdminNote note)
- {
- await using var db = await GetDb();
- db.DbContext.AdminNotes.Add(note);
- await db.DbContext.SaveChangesAsync();
- return note.Id;
- }
- public virtual async Task<int> AddAdminWatchlist(AdminWatchlist watchlist)
- {
- await using var db = await GetDb();
- db.DbContext.AdminWatchlists.Add(watchlist);
- await db.DbContext.SaveChangesAsync();
- return watchlist.Id;
- }
- public virtual async Task<int> AddAdminMessage(AdminMessage message)
- {
- await using var db = await GetDb();
- db.DbContext.AdminMessages.Add(message);
- await db.DbContext.SaveChangesAsync();
- return message.Id;
- }
- public async Task<AdminNoteRecord?> GetAdminNote(int id)
- {
- await using var db = await GetDb();
- var entity = await db.DbContext.AdminNotes
- .Where(note => note.Id == id)
- .Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.LastEditedBy)
- .Include(note => note.DeletedBy)
- .Include(note => note.Player)
- .SingleOrDefaultAsync();
- return entity == null ? null : MakeAdminNoteRecord(entity);
- }
- private AdminNoteRecord MakeAdminNoteRecord(AdminNote entity)
- {
- return new AdminNoteRecord(
- entity.Id,
- MakeRoundRecord(entity.Round),
- MakePlayerRecord(entity.Player),
- entity.PlaytimeAtNote,
- entity.Message,
- entity.Severity,
- MakePlayerRecord(entity.CreatedBy),
- NormalizeDatabaseTime(entity.CreatedAt),
- MakePlayerRecord(entity.LastEditedBy),
- NormalizeDatabaseTime(entity.LastEditedAt),
- NormalizeDatabaseTime(entity.ExpirationTime),
- entity.Deleted,
- MakePlayerRecord(entity.DeletedBy),
- NormalizeDatabaseTime(entity.DeletedAt),
- entity.Secret);
- }
- public async Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
- {
- await using var db = await GetDb();
- var entity = await db.DbContext.AdminWatchlists
- .Where(note => note.Id == id)
- .Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.LastEditedBy)
- .Include(note => note.DeletedBy)
- .Include(note => note.Player)
- .SingleOrDefaultAsync();
- return entity == null ? null : MakeAdminWatchlistRecord(entity);
- }
- public async Task<AdminMessageRecord?> GetAdminMessage(int id)
- {
- await using var db = await GetDb();
- var entity = await db.DbContext.AdminMessages
- .Where(note => note.Id == id)
- .Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.LastEditedBy)
- .Include(note => note.DeletedBy)
- .Include(note => note.Player)
- .SingleOrDefaultAsync();
- return entity == null ? null : MakeAdminMessageRecord(entity);
- }
- private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity)
- {
- return new AdminMessageRecord(
- entity.Id,
- MakeRoundRecord(entity.Round),
- MakePlayerRecord(entity.Player),
- entity.PlaytimeAtNote,
- entity.Message,
- MakePlayerRecord(entity.CreatedBy),
- NormalizeDatabaseTime(entity.CreatedAt),
- MakePlayerRecord(entity.LastEditedBy),
- NormalizeDatabaseTime(entity.LastEditedAt),
- NormalizeDatabaseTime(entity.ExpirationTime),
- entity.Deleted,
- MakePlayerRecord(entity.DeletedBy),
- NormalizeDatabaseTime(entity.DeletedAt),
- entity.Seen,
- entity.Dismissed);
- }
- public async Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
- {
- await using var db = await GetDb();
- var ban = await db.DbContext.Ban
- .Include(ban => ban.Unban)
- .Include(ban => ban.Round)
- .ThenInclude(r => r!.Server)
- .Include(ban => ban.CreatedBy)
- .Include(ban => ban.LastEditedBy)
- .Include(ban => ban.Unban)
- .SingleOrDefaultAsync(b => b.Id == id);
- if (ban is null)
- return null;
- var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
- return new ServerBanNoteRecord(
- ban.Id,
- MakeRoundRecord(ban.Round),
- MakePlayerRecord(player),
- ban.PlaytimeAtNote,
- ban.Reason,
- ban.Severity,
- MakePlayerRecord(ban.CreatedBy),
- ban.BanTime,
- MakePlayerRecord(ban.LastEditedBy),
- ban.LastEditedAt,
- ban.ExpirationTime,
- ban.Hidden,
- MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
- ? null
- : await db.DbContext.Player.SingleOrDefaultAsync(p =>
- p.UserId == ban.Unban.UnbanningAdmin.Value)),
- ban.Unban?.UnbanTime);
- }
- public async Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
- {
- await using var db = await GetDb();
- var ban = await db.DbContext.RoleBan
- .Include(ban => ban.Unban)
- .Include(ban => ban.Round)
- .ThenInclude(r => r!.Server)
- .Include(ban => ban.CreatedBy)
- .Include(ban => ban.LastEditedBy)
- .Include(ban => ban.Unban)
- .SingleOrDefaultAsync(b => b.Id == id);
- if (ban is null)
- return null;
- var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
- var unbanningAdmin =
- ban.Unban is null
- ? null
- : await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin);
- return new ServerRoleBanNoteRecord(
- ban.Id,
- MakeRoundRecord(ban.Round),
- MakePlayerRecord(player),
- ban.PlaytimeAtNote,
- ban.Reason,
- ban.Severity,
- MakePlayerRecord(ban.CreatedBy),
- ban.BanTime,
- MakePlayerRecord(ban.LastEditedBy),
- ban.LastEditedAt,
- ban.ExpirationTime,
- ban.Hidden,
- new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
- MakePlayerRecord(unbanningAdmin),
- ban.Unban?.UnbanTime);
- }
- public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
- {
- await using var db = await GetDb();
- List<IAdminRemarksRecord> notes = new();
- notes.AddRange(
- (await (from note in db.DbContext.AdminNotes
- where note.PlayerUserId == player &&
- !note.Deleted &&
- (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
- select note)
- .Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.LastEditedBy)
- .Include(note => note.Player)
- .ToListAsync()).Select(MakeAdminNoteRecord));
- notes.AddRange(await GetActiveWatchlistsImpl(db, player));
- notes.AddRange(await GetMessagesImpl(db, player));
- notes.AddRange(await GetServerBansAsNotesForUser(db, player));
- notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
- return notes;
- }
- public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
- {
- await using var db = await GetDb();
- var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
- note.Message = message;
- note.Severity = severity;
- note.Secret = secret;
- note.LastEditedById = editedBy;
- note.LastEditedAt = editedAt.UtcDateTime;
- note.ExpirationTime = expiryTime?.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
- {
- await using var db = await GetDb();
- var note = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync();
- note.Message = message;
- note.LastEditedById = editedBy;
- note.LastEditedAt = editedAt.UtcDateTime;
- note.ExpirationTime = expiryTime?.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
- {
- await using var db = await GetDb();
- var note = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync();
- note.Message = message;
- note.LastEditedById = editedBy;
- note.LastEditedAt = editedAt.UtcDateTime;
- note.ExpirationTime = expiryTime?.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- await using var db = await GetDb();
- var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
- note.Deleted = true;
- note.DeletedById = deletedBy;
- note.DeletedAt = deletedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- await using var db = await GetDb();
- var watchlist = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync();
- watchlist.Deleted = true;
- watchlist.DeletedById = deletedBy;
- watchlist.DeletedAt = deletedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- await using var db = await GetDb();
- var message = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync();
- message.Deleted = true;
- message.DeletedById = deletedBy;
- message.DeletedAt = deletedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- await using var db = await GetDb();
- var ban = await db.DbContext.Ban.Where(ban => ban.Id == id).SingleAsync();
- ban.Hidden = true;
- ban.LastEditedById = deletedBy;
- ban.LastEditedAt = deletedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
- {
- await using var db = await GetDb();
- var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync();
- roleBan.Hidden = true;
- roleBan.LastEditedById = deletedBy;
- roleBan.LastEditedAt = deletedAt.UtcDateTime;
- await db.DbContext.SaveChangesAsync();
- }
- public async Task<List<IAdminRemarksRecord>> GetVisibleAdminRemarks(Guid player)
- {
- await using var db = await GetDb();
- List<IAdminRemarksRecord> notesCol = new();
- notesCol.AddRange(
- (await (from note in db.DbContext.AdminNotes
- where note.PlayerUserId == player &&
- !note.Secret &&
- !note.Deleted &&
- (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
- select note)
- .Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.Player)
- .ToListAsync()).Select(MakeAdminNoteRecord));
- notesCol.AddRange(await GetMessagesImpl(db, player));
- notesCol.AddRange(await GetServerBansAsNotesForUser(db, player));
- notesCol.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
- return notesCol;
- }
- public async Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
- {
- await using var db = await GetDb();
- return await GetActiveWatchlistsImpl(db, player);
- }
- protected async Task<List<AdminWatchlistRecord>> GetActiveWatchlistsImpl(DbGuard db, Guid player)
- {
- var entities = await (from watchlist in db.DbContext.AdminWatchlists
- where watchlist.PlayerUserId == player &&
- !watchlist.Deleted &&
- (watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime)
- select watchlist)
- .Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.LastEditedBy)
- .Include(note => note.Player)
- .ToListAsync();
- return entities.Select(MakeAdminWatchlistRecord).ToList();
- }
- private AdminWatchlistRecord MakeAdminWatchlistRecord(AdminWatchlist entity)
- {
- return new AdminWatchlistRecord(entity.Id, MakeRoundRecord(entity.Round), MakePlayerRecord(entity.Player), entity.PlaytimeAtNote, entity.Message, MakePlayerRecord(entity.CreatedBy), NormalizeDatabaseTime(entity.CreatedAt), MakePlayerRecord(entity.LastEditedBy), NormalizeDatabaseTime(entity.LastEditedAt), NormalizeDatabaseTime(entity.ExpirationTime), entity.Deleted, MakePlayerRecord(entity.DeletedBy), NormalizeDatabaseTime(entity.DeletedAt));
- }
- public async Task<List<AdminMessageRecord>> GetMessages(Guid player)
- {
- await using var db = await GetDb();
- return await GetMessagesImpl(db, player);
- }
- protected async Task<List<AdminMessageRecord>> GetMessagesImpl(DbGuard db, Guid player)
- {
- var entities = await (from message in db.DbContext.AdminMessages
- where message.PlayerUserId == player && !message.Deleted &&
- (message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime)
- select message).Include(note => note.Round)
- .ThenInclude(r => r!.Server)
- .Include(note => note.CreatedBy)
- .Include(note => note.LastEditedBy)
- .Include(note => note.Player)
- .ToListAsync();
- return entities.Select(MakeAdminMessageRecord).ToList();
- }
- public async Task MarkMessageAsSeen(int id, bool dismissedToo)
- {
- await using var db = await GetDb();
- var message = await db.DbContext.AdminMessages.SingleAsync(m => m.Id == id);
- message.Seen = true;
- if (dismissedToo)
- message.Dismissed = true;
- await db.DbContext.SaveChangesAsync();
- }
- // These two are here because they get converted into notes later
- protected async Task<List<ServerBanNoteRecord>> GetServerBansAsNotesForUser(DbGuard db, Guid user)
- {
- // You can't group queries, as player will not always exist. When it doesn't, the
- // whole query returns nothing
- var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
- var bans = await db.DbContext.Ban
- .Where(ban => ban.PlayerUserId == user && !ban.Hidden)
- .Include(ban => ban.Unban)
- .Include(ban => ban.Round)
- .ThenInclude(r => r!.Server)
- .Include(ban => ban.CreatedBy)
- .Include(ban => ban.LastEditedBy)
- .Include(ban => ban.Unban)
- .ToArrayAsync();
- var banNotes = new List<ServerBanNoteRecord>();
- foreach (var ban in bans)
- {
- var banNote = new ServerBanNoteRecord(
- ban.Id,
- MakeRoundRecord(ban.Round),
- MakePlayerRecord(player),
- ban.PlaytimeAtNote,
- ban.Reason,
- ban.Severity,
- MakePlayerRecord(ban.CreatedBy),
- NormalizeDatabaseTime(ban.BanTime),
- MakePlayerRecord(ban.LastEditedBy),
- NormalizeDatabaseTime(ban.LastEditedAt),
- NormalizeDatabaseTime(ban.ExpirationTime),
- ban.Hidden,
- MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
- ? null
- : await db.DbContext.Player.SingleOrDefaultAsync(
- p => p.UserId == ban.Unban.UnbanningAdmin.Value)),
- NormalizeDatabaseTime(ban.Unban?.UnbanTime));
- banNotes.Add(banNote);
- }
- return banNotes;
- }
- protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
- {
- // Server side query
- var bansQuery = await db.DbContext.RoleBan
- .Where(ban => ban.PlayerUserId == user && !ban.Hidden)
- .Include(ban => ban.Unban)
- .Include(ban => ban.Round)
- .ThenInclude(r => r!.Server)
- .Include(ban => ban.CreatedBy)
- .Include(ban => ban.LastEditedBy)
- .Include(ban => ban.Unban)
- .ToArrayAsync();
- // Client side query, as EF can't do groups yet
- var bansEnumerable = bansQuery
- .GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null })
- .Select(banGroup => banGroup)
- .ToArray();
- List<ServerRoleBanNoteRecord> bans = new();
- var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
- foreach (var banGroup in bansEnumerable)
- {
- var firstBan = banGroup.First();
- Player? unbanningAdmin = null;
- if (firstBan.Unban?.UnbanningAdmin is not null)
- unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value);
- bans.Add(new ServerRoleBanNoteRecord(
- firstBan.Id,
- MakeRoundRecord(firstBan.Round),
- MakePlayerRecord(player),
- firstBan.PlaytimeAtNote,
- firstBan.Reason,
- firstBan.Severity,
- MakePlayerRecord(firstBan.CreatedBy),
- NormalizeDatabaseTime(firstBan.BanTime),
- MakePlayerRecord(firstBan.LastEditedBy),
- NormalizeDatabaseTime(firstBan.LastEditedAt),
- NormalizeDatabaseTime(firstBan.ExpirationTime),
- firstBan.Hidden,
- banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(),
- MakePlayerRecord(unbanningAdmin),
- NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
- }
- return bans;
- }
- #endregion
- #region Job Whitelists
- public async Task<bool> AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
- {
- await using var db = await GetDb();
- var exists = await db.DbContext.RoleWhitelists
- .Where(w => w.PlayerUserId == player)
- .Where(w => w.RoleId == job.Id)
- .AnyAsync();
- if (exists)
- return false;
- var whitelist = new RoleWhitelist
- {
- PlayerUserId = player,
- RoleId = job
- };
- db.DbContext.RoleWhitelists.Add(whitelist);
- await db.DbContext.SaveChangesAsync();
- return true;
- }
- public async Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel)
- {
- await using var db = await GetDb(cancel);
- return await db.DbContext.RoleWhitelists
- .Where(w => w.PlayerUserId == player)
- .Select(w => w.RoleId)
- .ToListAsync(cancellationToken: cancel);
- }
- public async Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
- {
- await using var db = await GetDb();
- return await db.DbContext.RoleWhitelists
- .Where(w => w.PlayerUserId == player)
- .Where(w => w.RoleId == job.Id)
- .AnyAsync();
- }
- public async Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
- {
- await using var db = await GetDb();
- var entry = await db.DbContext.RoleWhitelists
- .Where(w => w.PlayerUserId == player)
- .Where(w => w.RoleId == job.Id)
- .SingleOrDefaultAsync();
- if (entry == null)
- return false;
- db.DbContext.RoleWhitelists.Remove(entry);
- await db.DbContext.SaveChangesAsync();
- return true;
- }
- #endregion
- # region IPIntel
- public async Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
- {
- while (true)
- {
- try
- {
- await using var db = await GetDb();
- var existing = await db.DbContext.IPIntelCache
- .Where(w => ip.Equals(w.Address))
- .SingleOrDefaultAsync();
- if (existing == null)
- {
- var newCache = new IPIntelCache
- {
- Time = time,
- Address = ip,
- Score = score,
- };
- db.DbContext.IPIntelCache.Add(newCache);
- }
- else
- {
- existing.Time = time;
- existing.Score = score;
- }
- await Task.Delay(5000);
- await db.DbContext.SaveChangesAsync();
- return true;
- }
- catch (DbUpdateException)
- {
- _opsLog.Warning("IPIntel UPSERT failed with a db exception... retrying.");
- }
- }
- }
- public async Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
- {
- await using var db = await GetDb();
- return await db.DbContext.IPIntelCache
- .SingleOrDefaultAsync(w => ip.Equals(w.Address));
- }
- public async Task<bool> CleanIPIntelCache(TimeSpan range)
- {
- await using var db = await GetDb();
- // Calculating this here cause otherwise sqlite whines.
- var cutoffTime = DateTime.UtcNow.Subtract(range);
- await db.DbContext.IPIntelCache
- .Where(w => w.Time <= cutoffTime)
- .ExecuteDeleteAsync();
- await db.DbContext.SaveChangesAsync();
- return true;
- }
- #endregion
- public abstract Task SendNotification(DatabaseNotification notification);
- // SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
- // Normalize DateTimes here so they're always Utc. Thanks.
- protected abstract DateTime NormalizeDatabaseTime(DateTime time);
- [return: NotNullIfNotNull(nameof(time))]
- protected DateTime? NormalizeDatabaseTime(DateTime? time)
- {
- return time != null ? NormalizeDatabaseTime(time.Value) : time;
- }
- public async Task<bool> HasPendingModelChanges()
- {
- await using var db = await GetDb();
- return db.DbContext.Database.HasPendingModelChanges();
- }
- protected abstract Task<DbGuard> GetDb(
- CancellationToken cancel = default,
- [CallerMemberName] string? name = null);
- protected void LogDbOp(string? name)
- {
- _opsLog.Verbose($"Running DB operation: {name ?? "unknown"}");
- }
- protected abstract class DbGuard : IAsyncDisposable
- {
- public abstract ServerDbContext DbContext { get; }
- public abstract ValueTask DisposeAsync();
- }
- protected void NotificationReceived(DatabaseNotification notification)
- {
- OnNotificationReceived?.Invoke(notification);
- }
- public virtual void Shutdown()
- {
- }
- }
- }
|