1
0

ServerDbBase.cs 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850
  1. using System.Collections.Immutable;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Runtime.CompilerServices;
  6. using System.Text.Json;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Content.Server.Administration.Logs;
  10. using Content.Server.Administration.Managers;
  11. using Content.Shared.Administration.Logs;
  12. using Content.Shared.Database;
  13. using Content.Shared.Humanoid;
  14. using Content.Shared.Humanoid.Markings;
  15. using Content.Shared.Preferences;
  16. using Content.Shared.Preferences.Loadouts;
  17. using Content.Shared.Roles;
  18. using Content.Shared.Traits;
  19. using Microsoft.EntityFrameworkCore;
  20. using Robust.Shared.Enums;
  21. using Robust.Shared.Network;
  22. using Robust.Shared.Prototypes;
  23. using Robust.Shared.Utility;
  24. namespace Content.Server.Database
  25. {
  26. public abstract class ServerDbBase
  27. {
  28. private readonly ISawmill _opsLog;
  29. public event Action<DatabaseNotification>? OnNotificationReceived;
  30. /// <param name="opsLog">Sawmill to trace log database operations to.</param>
  31. public ServerDbBase(ISawmill opsLog)
  32. {
  33. _opsLog = opsLog;
  34. }
  35. #region Preferences
  36. public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(
  37. NetUserId userId,
  38. CancellationToken cancel = default)
  39. {
  40. await using var db = await GetDb(cancel);
  41. var prefs = await db.DbContext
  42. .Preference
  43. .Include(p => p.Profiles).ThenInclude(h => h.Jobs)
  44. .Include(p => p.Profiles).ThenInclude(h => h.Antags)
  45. .Include(p => p.Profiles).ThenInclude(h => h.Traits)
  46. .Include(p => p.Profiles)
  47. .ThenInclude(h => h.Loadouts)
  48. .ThenInclude(l => l.Groups)
  49. .ThenInclude(group => group.Loadouts)
  50. .AsSplitQuery()
  51. .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
  52. if (prefs is null)
  53. return null;
  54. var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1;
  55. var profiles = new Dictionary<int, ICharacterProfile>(maxSlot);
  56. foreach (var profile in prefs.Profiles)
  57. {
  58. profiles[profile.Slot] = ConvertProfiles(profile);
  59. }
  60. return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor));
  61. }
  62. public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
  63. {
  64. await using var db = await GetDb();
  65. await SetSelectedCharacterSlotAsync(userId, index, db.DbContext);
  66. await db.DbContext.SaveChangesAsync();
  67. }
  68. public async Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
  69. {
  70. await using var db = await GetDb();
  71. if (profile is null)
  72. {
  73. await DeleteCharacterSlot(db.DbContext, userId, slot);
  74. await db.DbContext.SaveChangesAsync();
  75. return;
  76. }
  77. if (profile is not HumanoidCharacterProfile humanoid)
  78. {
  79. // TODO: Handle other ICharacterProfile implementations properly
  80. throw new NotImplementedException();
  81. }
  82. var oldProfile = db.DbContext.Profile
  83. .Include(p => p.Preference)
  84. .Where(p => p.Preference.UserId == userId.UserId)
  85. .Include(p => p.Jobs)
  86. .Include(p => p.Antags)
  87. .Include(p => p.Traits)
  88. .Include(p => p.Loadouts)
  89. .ThenInclude(l => l.Groups)
  90. .ThenInclude(group => group.Loadouts)
  91. .AsSplitQuery()
  92. .SingleOrDefault(h => h.Slot == slot);
  93. var newProfile = ConvertProfiles(humanoid, slot, oldProfile);
  94. if (oldProfile == null)
  95. {
  96. var prefs = await db.DbContext
  97. .Preference
  98. .Include(p => p.Profiles)
  99. .SingleAsync(p => p.UserId == userId.UserId);
  100. prefs.Profiles.Add(newProfile);
  101. }
  102. await db.DbContext.SaveChangesAsync();
  103. }
  104. private static async Task DeleteCharacterSlot(ServerDbContext db, NetUserId userId, int slot)
  105. {
  106. var profile = await db.Profile.Include(p => p.Preference)
  107. .Where(p => p.Preference.UserId == userId.UserId && p.Slot == slot)
  108. .SingleOrDefaultAsync();
  109. if (profile == null)
  110. {
  111. return;
  112. }
  113. db.Profile.Remove(profile);
  114. }
  115. public async Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
  116. {
  117. await using var db = await GetDb();
  118. var profile = ConvertProfiles((HumanoidCharacterProfile) defaultProfile, 0);
  119. var prefs = new Preference
  120. {
  121. UserId = userId.UserId,
  122. SelectedCharacterSlot = 0,
  123. AdminOOCColor = Color.Red.ToHex()
  124. };
  125. prefs.Profiles.Add(profile);
  126. db.DbContext.Preference.Add(prefs);
  127. await db.DbContext.SaveChangesAsync();
  128. return new PlayerPreferences(new[] {new KeyValuePair<int, ICharacterProfile>(0, defaultProfile)}, 0, Color.FromHex(prefs.AdminOOCColor));
  129. }
  130. public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
  131. {
  132. await using var db = await GetDb();
  133. await DeleteCharacterSlot(db.DbContext, userId, deleteSlot);
  134. await SetSelectedCharacterSlotAsync(userId, newSlot, db.DbContext);
  135. await db.DbContext.SaveChangesAsync();
  136. }
  137. public async Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
  138. {
  139. await using var db = await GetDb();
  140. var prefs = await db.DbContext
  141. .Preference
  142. .Include(p => p.Profiles)
  143. .SingleAsync(p => p.UserId == userId.UserId);
  144. prefs.AdminOOCColor = color.ToHex();
  145. await db.DbContext.SaveChangesAsync();
  146. }
  147. private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int newSlot, ServerDbContext db)
  148. {
  149. var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId);
  150. prefs.SelectedCharacterSlot = newSlot;
  151. }
  152. private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
  153. {
  154. var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
  155. var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
  156. var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
  157. var sex = Sex.Male;
  158. if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
  159. sex = sexVal;
  160. var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
  161. var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
  162. if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
  163. gender = genderVal;
  164. // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
  165. var markingsRaw = profile.Markings?.Deserialize<List<string>>();
  166. List<Marking> markings = new();
  167. if (markingsRaw != null)
  168. {
  169. foreach (var marking in markingsRaw)
  170. {
  171. var parsed = Marking.ParseFromDbString(marking);
  172. if (parsed is null) continue;
  173. markings.Add(parsed);
  174. }
  175. }
  176. var loadouts = new Dictionary<string, RoleLoadout>();
  177. foreach (var role in profile.Loadouts)
  178. {
  179. var loadout = new RoleLoadout(role.RoleName)
  180. {
  181. EntityName = role.EntityName,
  182. };
  183. foreach (var group in role.Groups)
  184. {
  185. var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
  186. foreach (var profLoadout in group.Loadouts)
  187. {
  188. groupLoadouts.Add(new Loadout()
  189. {
  190. Prototype = profLoadout.LoadoutName,
  191. });
  192. }
  193. }
  194. loadouts[role.RoleName] = loadout;
  195. }
  196. return new HumanoidCharacterProfile(
  197. profile.CharacterName,
  198. profile.FlavorText,
  199. profile.Species,
  200. profile.Age,
  201. sex,
  202. gender,
  203. new HumanoidCharacterAppearance
  204. (
  205. profile.HairName,
  206. Color.FromHex(profile.HairColor),
  207. profile.FacialHairName,
  208. Color.FromHex(profile.FacialHairColor),
  209. Color.FromHex(profile.EyeColor),
  210. Color.FromHex(profile.SkinColor),
  211. markings
  212. ),
  213. spawnPriority,
  214. jobs,
  215. (PreferenceUnavailableMode) profile.PreferenceUnavailable,
  216. antags.ToHashSet(),
  217. traits.ToHashSet(),
  218. loadouts
  219. );
  220. }
  221. private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
  222. {
  223. profile ??= new Profile();
  224. var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
  225. List<string> markingStrings = new();
  226. foreach (var marking in appearance.Markings)
  227. {
  228. markingStrings.Add(marking.ToString());
  229. }
  230. var markings = JsonSerializer.SerializeToDocument(markingStrings);
  231. profile.CharacterName = humanoid.Name;
  232. profile.FlavorText = humanoid.FlavorText;
  233. profile.Species = humanoid.Species;
  234. profile.Age = humanoid.Age;
  235. profile.Sex = humanoid.Sex.ToString();
  236. profile.Gender = humanoid.Gender.ToString();
  237. profile.HairName = appearance.HairStyleId;
  238. profile.HairColor = appearance.HairColor.ToHex();
  239. profile.FacialHairName = appearance.FacialHairStyleId;
  240. profile.FacialHairColor = appearance.FacialHairColor.ToHex();
  241. profile.EyeColor = appearance.EyeColor.ToHex();
  242. profile.SkinColor = appearance.SkinColor.ToHex();
  243. profile.SpawnPriority = (int) humanoid.SpawnPriority;
  244. profile.Markings = markings;
  245. profile.Slot = slot;
  246. profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
  247. profile.Jobs.Clear();
  248. profile.Jobs.AddRange(
  249. humanoid.JobPriorities
  250. .Where(j => j.Value != JobPriority.Never)
  251. .Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
  252. );
  253. profile.Antags.Clear();
  254. profile.Antags.AddRange(
  255. humanoid.AntagPreferences
  256. .Select(a => new Antag {AntagName = a})
  257. );
  258. profile.Traits.Clear();
  259. profile.Traits.AddRange(
  260. humanoid.TraitPreferences
  261. .Select(t => new Trait {TraitName = t})
  262. );
  263. profile.Loadouts.Clear();
  264. foreach (var (role, loadouts) in humanoid.Loadouts)
  265. {
  266. var dz = new ProfileRoleLoadout()
  267. {
  268. RoleName = role,
  269. EntityName = loadouts.EntityName ?? string.Empty,
  270. };
  271. foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
  272. {
  273. var profileGroup = new ProfileLoadoutGroup()
  274. {
  275. GroupName = group,
  276. };
  277. foreach (var loadout in groupLoadouts)
  278. {
  279. profileGroup.Loadouts.Add(new ProfileLoadout()
  280. {
  281. LoadoutName = loadout.Prototype,
  282. });
  283. }
  284. dz.Groups.Add(profileGroup);
  285. }
  286. profile.Loadouts.Add(dz);
  287. }
  288. return profile;
  289. }
  290. #endregion
  291. #region User Ids
  292. public async Task<NetUserId?> GetAssignedUserIdAsync(string name)
  293. {
  294. await using var db = await GetDb();
  295. var assigned = await db.DbContext.AssignedUserId.SingleOrDefaultAsync(p => p.UserName == name);
  296. return assigned?.UserId is { } g ? new NetUserId(g) : default(NetUserId?);
  297. }
  298. public async Task AssignUserIdAsync(string name, NetUserId netUserId)
  299. {
  300. await using var db = await GetDb();
  301. db.DbContext.AssignedUserId.Add(new AssignedUserId
  302. {
  303. UserId = netUserId.UserId,
  304. UserName = name
  305. });
  306. await db.DbContext.SaveChangesAsync();
  307. }
  308. #endregion
  309. #region Bans
  310. /*
  311. * BAN STUFF
  312. */
  313. /// <summary>
  314. /// Looks up a ban by id.
  315. /// This will return a pardoned ban as well.
  316. /// </summary>
  317. /// <param name="id">The ban id to look for.</param>
  318. /// <returns>The ban with the given id or null if none exist.</returns>
  319. public abstract Task<ServerBanDef?> GetServerBanAsync(int id);
  320. /// <summary>
  321. /// Looks up an user's most recent received un-pardoned ban.
  322. /// This will NOT return a pardoned ban.
  323. /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
  324. /// </summary>
  325. /// <param name="address">The ip address of the user.</param>
  326. /// <param name="userId">The id of the user.</param>
  327. /// <param name="hwId">The legacy HWId of the user.</param>
  328. /// <param name="modernHWIds">The modern HWIDs of the user.</param>
  329. /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
  330. public abstract Task<ServerBanDef?> GetServerBanAsync(
  331. IPAddress? address,
  332. NetUserId? userId,
  333. ImmutableArray<byte>? hwId,
  334. ImmutableArray<ImmutableArray<byte>>? modernHWIds);
  335. /// <summary>
  336. /// Looks up an user's ban history.
  337. /// This will return pardoned bans as well.
  338. /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
  339. /// </summary>
  340. /// <param name="address">The ip address of the user.</param>
  341. /// <param name="userId">The id of the user.</param>
  342. /// <param name="hwId">The legacy HWId of the user.</param>
  343. /// <param name="modernHWIds">The modern HWIDs of the user.</param>
  344. /// <param name="includeUnbanned">Include pardoned and expired bans.</param>
  345. /// <returns>The user's ban history.</returns>
  346. public abstract Task<List<ServerBanDef>> GetServerBansAsync(
  347. IPAddress? address,
  348. NetUserId? userId,
  349. ImmutableArray<byte>? hwId,
  350. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  351. bool includeUnbanned);
  352. public abstract Task AddServerBanAsync(ServerBanDef serverBan);
  353. public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
  354. public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
  355. {
  356. await using var db = await GetDb();
  357. var ban = await db.DbContext.Ban.SingleOrDefaultAsync(b => b.Id == id);
  358. if (ban is null)
  359. return;
  360. ban.Severity = severity;
  361. ban.Reason = reason;
  362. ban.ExpirationTime = expiration?.UtcDateTime;
  363. ban.LastEditedById = editedBy;
  364. ban.LastEditedAt = editedAt.UtcDateTime;
  365. await db.DbContext.SaveChangesAsync();
  366. }
  367. protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(
  368. DbGuard db,
  369. NetUserId? userId,
  370. CancellationToken cancel = default)
  371. {
  372. if (userId == null)
  373. return null;
  374. var exemption = await db.DbContext.BanExemption
  375. .SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId, cancellationToken: cancel);
  376. return exemption?.Flags;
  377. }
  378. public async Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
  379. {
  380. await using var db = await GetDb();
  381. if (flags == 0)
  382. {
  383. // Delete whatever is there.
  384. await db.DbContext.BanExemption.Where(u => u.UserId == userId.UserId).ExecuteDeleteAsync();
  385. return;
  386. }
  387. var exemption = await db.DbContext.BanExemption.SingleOrDefaultAsync(u => u.UserId == userId.UserId);
  388. if (exemption == null)
  389. {
  390. exemption = new ServerBanExemption
  391. {
  392. UserId = userId
  393. };
  394. db.DbContext.BanExemption.Add(exemption);
  395. }
  396. exemption.Flags = flags;
  397. await db.DbContext.SaveChangesAsync();
  398. }
  399. public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel)
  400. {
  401. await using var db = await GetDb(cancel);
  402. var flags = await GetBanExemptionCore(db, userId, cancel);
  403. return flags ?? ServerBanExemptFlags.None;
  404. }
  405. #endregion
  406. #region Role Bans
  407. /*
  408. * ROLE BANS
  409. */
  410. /// <summary>
  411. /// Looks up a role ban by id.
  412. /// This will return a pardoned role ban as well.
  413. /// </summary>
  414. /// <param name="id">The role ban id to look for.</param>
  415. /// <returns>The role ban with the given id or null if none exist.</returns>
  416. public abstract Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
  417. /// <summary>
  418. /// Looks up an user's role ban history.
  419. /// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
  420. /// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
  421. /// </summary>
  422. /// <param name="address">The IP address of the user.</param>
  423. /// <param name="userId">The NetUserId of the user.</param>
  424. /// <param name="hwId">The Hardware Id of the user.</param>
  425. /// <param name="modernHWIds">The modern HWIDs of the user.</param>
  426. /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
  427. /// <returns>The user's role ban history.</returns>
  428. public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
  429. NetUserId? userId,
  430. ImmutableArray<byte>? hwId,
  431. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  432. bool includeUnbanned);
  433. public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
  434. public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
  435. public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
  436. {
  437. await using var db = await GetDb();
  438. var roleBanDetails = await db.DbContext.RoleBan
  439. .Where(b => b.Id == id)
  440. .Select(b => new { b.BanTime, b.PlayerUserId })
  441. .SingleOrDefaultAsync();
  442. if (roleBanDetails == default)
  443. return;
  444. await db.DbContext.RoleBan
  445. .Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId)
  446. .ExecuteUpdateAsync(setters => setters
  447. .SetProperty(b => b.Severity, severity)
  448. .SetProperty(b => b.Reason, reason)
  449. .SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null)
  450. .SetProperty(b => b.LastEditedById, editedBy)
  451. .SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime)
  452. );
  453. }
  454. #endregion
  455. #region Playtime
  456. public async Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
  457. {
  458. await using var db = await GetDb(cancel);
  459. return await db.DbContext.PlayTime
  460. .Where(p => p.PlayerId == player)
  461. .ToListAsync(cancel);
  462. }
  463. public async Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
  464. {
  465. await using var db = await GetDb();
  466. // Ideally I would just be able to send a bunch of UPSERT commands, but EFCore is a pile of garbage.
  467. // So... In the interest of not making this take forever at high update counts...
  468. // Bulk-load play time objects for all players involved.
  469. // This allows us to semi-efficiently load all entities we need in a single DB query.
  470. // Then we can update & insert without further round-trips to the DB.
  471. var players = updates.Select(u => u.User.UserId).Distinct().ToArray();
  472. var dbTimes = (await db.DbContext.PlayTime
  473. .Where(p => players.Contains(p.PlayerId))
  474. .ToArrayAsync())
  475. .GroupBy(p => p.PlayerId)
  476. .ToDictionary(g => g.Key, g => g.ToDictionary(p => p.Tracker, p => p));
  477. foreach (var (user, tracker, time) in updates)
  478. {
  479. if (dbTimes.TryGetValue(user.UserId, out var userTimes)
  480. && userTimes.TryGetValue(tracker, out var ent))
  481. {
  482. // Already have a tracker in the database, update it.
  483. ent.TimeSpent = time;
  484. continue;
  485. }
  486. // No tracker, make a new one.
  487. var playTime = new PlayTime
  488. {
  489. Tracker = tracker,
  490. PlayerId = user.UserId,
  491. TimeSpent = time
  492. };
  493. db.DbContext.PlayTime.Add(playTime);
  494. }
  495. await db.DbContext.SaveChangesAsync();
  496. }
  497. #endregion
  498. #region Player Records
  499. /*
  500. * PLAYER RECORDS
  501. */
  502. public async Task UpdatePlayerRecord(
  503. NetUserId userId,
  504. string userName,
  505. IPAddress address,
  506. ImmutableTypedHwid? hwId)
  507. {
  508. await using var db = await GetDb();
  509. var record = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
  510. if (record == null)
  511. {
  512. db.DbContext.Player.Add(record = new Player
  513. {
  514. FirstSeenTime = DateTime.UtcNow,
  515. UserId = userId.UserId,
  516. });
  517. }
  518. record.LastSeenTime = DateTime.UtcNow;
  519. record.LastSeenAddress = address;
  520. record.LastSeenUserName = userName;
  521. record.LastSeenHWId = hwId;
  522. await db.DbContext.SaveChangesAsync();
  523. }
  524. public async Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel)
  525. {
  526. await using var db = await GetDb();
  527. // Sort by descending last seen time.
  528. // So if, due to account renames, we have two people with the same username in the DB,
  529. // the most recent one is picked.
  530. var record = await db.DbContext.Player
  531. .OrderByDescending(p => p.LastSeenTime)
  532. .FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel);
  533. return record == null ? null : MakePlayerRecord(record);
  534. }
  535. public async Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel)
  536. {
  537. await using var db = await GetDb();
  538. var record = await db.DbContext.Player
  539. .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
  540. return record == null ? null : MakePlayerRecord(record);
  541. }
  542. protected async Task<bool> PlayerRecordExists(DbGuard db, NetUserId userId)
  543. {
  544. return await db.DbContext.Player.AnyAsync(p => p.UserId == userId);
  545. }
  546. [return: NotNullIfNotNull(nameof(player))]
  547. protected PlayerRecord? MakePlayerRecord(Player? player)
  548. {
  549. if (player == null)
  550. return null;
  551. return new PlayerRecord(
  552. new NetUserId(player.UserId),
  553. new DateTimeOffset(NormalizeDatabaseTime(player.FirstSeenTime)),
  554. player.LastSeenUserName,
  555. new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
  556. player.LastSeenAddress,
  557. player.LastSeenHWId);
  558. }
  559. #endregion
  560. #region Connection Logs
  561. /*
  562. * CONNECTION LOG
  563. */
  564. public abstract Task<int> AddConnectionLogAsync(NetUserId userId,
  565. string userName,
  566. IPAddress address,
  567. ImmutableTypedHwid? hwId,
  568. float trust,
  569. ConnectionDenyReason? denied,
  570. int serverId);
  571. public async Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
  572. {
  573. await using var db = await GetDb();
  574. foreach (var ban in bans)
  575. {
  576. db.DbContext.ServerBanHit.Add(new ServerBanHit
  577. {
  578. ConnectionId = connection, BanId = ban.Id!.Value
  579. });
  580. }
  581. await db.DbContext.SaveChangesAsync();
  582. }
  583. #endregion
  584. #region Admin Ranks
  585. /*
  586. * ADMIN RANKS
  587. */
  588. public async Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel)
  589. {
  590. await using var db = await GetDb(cancel);
  591. return await db.DbContext.Admin
  592. .Include(p => p.Flags)
  593. .Include(p => p.AdminRank)
  594. .ThenInclude(p => p!.Flags)
  595. .AsSplitQuery() // tests fail because of a random warning if you dont have this!
  596. .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
  597. }
  598. public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])>
  599. GetAllAdminAndRanksAsync(CancellationToken cancel);
  600. public async Task<AdminRank?> GetAdminRankDataForAsync(int id, CancellationToken cancel = default)
  601. {
  602. await using var db = await GetDb(cancel);
  603. return await db.DbContext.AdminRank
  604. .Include(r => r.Flags)
  605. .SingleOrDefaultAsync(r => r.Id == id, cancel);
  606. }
  607. public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel)
  608. {
  609. await using var db = await GetDb(cancel);
  610. var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel);
  611. db.DbContext.Admin.Remove(admin);
  612. await db.DbContext.SaveChangesAsync(cancel);
  613. }
  614. public async Task AddAdminAsync(Admin admin, CancellationToken cancel)
  615. {
  616. await using var db = await GetDb(cancel);
  617. db.DbContext.Admin.Add(admin);
  618. await db.DbContext.SaveChangesAsync(cancel);
  619. }
  620. public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel)
  621. {
  622. await using var db = await GetDb(cancel);
  623. var existing = await db.DbContext.Admin.Include(a => a.Flags).SingleAsync(a => a.UserId == admin.UserId, cancel);
  624. existing.Flags = admin.Flags;
  625. existing.Title = admin.Title;
  626. existing.AdminRankId = admin.AdminRankId;
  627. existing.Deadminned = admin.Deadminned;
  628. existing.Suspended = admin.Suspended;
  629. await db.DbContext.SaveChangesAsync(cancel);
  630. }
  631. public async Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel)
  632. {
  633. await using var db = await GetDb(cancel);
  634. var adminRecord = db.DbContext.Admin.Where(a => a.UserId == userId);
  635. await adminRecord.ExecuteUpdateAsync(
  636. set => set.SetProperty(p => p.Deadminned, deadminned),
  637. cancellationToken: cancel);
  638. await db.DbContext.SaveChangesAsync(cancel);
  639. }
  640. public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel)
  641. {
  642. await using var db = await GetDb(cancel);
  643. var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel);
  644. db.DbContext.AdminRank.Remove(admin);
  645. await db.DbContext.SaveChangesAsync(cancel);
  646. }
  647. public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel)
  648. {
  649. await using var db = await GetDb(cancel);
  650. db.DbContext.AdminRank.Add(rank);
  651. await db.DbContext.SaveChangesAsync(cancel);
  652. }
  653. public async Task<int> AddNewRound(Server server, params Guid[] playerIds)
  654. {
  655. await using var db = await GetDb();
  656. var players = await db.DbContext.Player
  657. .Where(player => playerIds.Contains(player.UserId))
  658. .ToListAsync();
  659. var round = new Round
  660. {
  661. StartDate = DateTime.UtcNow,
  662. Players = players,
  663. ServerId = server.Id
  664. };
  665. db.DbContext.Round.Add(round);
  666. await db.DbContext.SaveChangesAsync();
  667. return round.Id;
  668. }
  669. public async Task<Round> GetRound(int id)
  670. {
  671. await using var db = await GetDb();
  672. var round = await db.DbContext.Round
  673. .Include(round => round.Players)
  674. .SingleAsync(round => round.Id == id);
  675. return round;
  676. }
  677. public async Task AddRoundPlayers(int id, Guid[] playerIds)
  678. {
  679. await using var db = await GetDb();
  680. // ReSharper disable once SuggestVarOrType_Elsewhere
  681. Dictionary<Guid, int> players = await db.DbContext.Player
  682. .Where(player => playerIds.Contains(player.UserId))
  683. .ToDictionaryAsync(player => player.UserId, player => player.Id);
  684. foreach (var player in playerIds)
  685. {
  686. await db.DbContext.Database.ExecuteSqlAsync($"""
  687. INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}) ON CONFLICT DO NOTHING
  688. """);
  689. }
  690. await db.DbContext.SaveChangesAsync();
  691. }
  692. [return: NotNullIfNotNull(nameof(round))]
  693. protected RoundRecord? MakeRoundRecord(Round? round)
  694. {
  695. if (round == null)
  696. return null;
  697. return new RoundRecord(
  698. round.Id,
  699. NormalizeDatabaseTime(round.StartDate),
  700. MakeServerRecord(round.Server));
  701. }
  702. public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel)
  703. {
  704. await using var db = await GetDb(cancel);
  705. var existing = await db.DbContext.AdminRank
  706. .Include(r => r.Flags)
  707. .SingleAsync(a => a.Id == rank.Id, cancel);
  708. existing.Flags = rank.Flags;
  709. existing.Name = rank.Name;
  710. await db.DbContext.SaveChangesAsync(cancel);
  711. }
  712. #endregion
  713. #region Admin Logs
  714. public async Task<(Server, bool existed)> AddOrGetServer(string serverName)
  715. {
  716. await using var db = await GetDb();
  717. var server = await db.DbContext.Server
  718. .Where(server => server.Name.Equals(serverName))
  719. .SingleOrDefaultAsync();
  720. if (server != default)
  721. return (server, true);
  722. server = new Server
  723. {
  724. Name = serverName
  725. };
  726. db.DbContext.Server.Add(server);
  727. await db.DbContext.SaveChangesAsync();
  728. return (server, false);
  729. }
  730. [return: NotNullIfNotNull(nameof(server))]
  731. protected ServerRecord? MakeServerRecord(Server? server)
  732. {
  733. if (server == null)
  734. return null;
  735. return new ServerRecord(server.Id, server.Name);
  736. }
  737. public async Task AddAdminLogs(List<AdminLog> logs)
  738. {
  739. const int maxRetryAttempts = 5;
  740. var initialRetryDelay = TimeSpan.FromSeconds(5);
  741. DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
  742. var attempt = 0;
  743. var retryDelay = initialRetryDelay;
  744. while (attempt < maxRetryAttempts)
  745. {
  746. try
  747. {
  748. await using var db = await GetDb();
  749. db.DbContext.AdminLog.AddRange(logs);
  750. await db.DbContext.SaveChangesAsync();
  751. _opsLog.Debug($"Successfully saved {logs.Count} admin logs.");
  752. break;
  753. }
  754. catch (Exception ex)
  755. {
  756. attempt += 1;
  757. _opsLog.Error($"Attempt {attempt} failed to save logs: {ex}");
  758. if (attempt >= maxRetryAttempts)
  759. {
  760. _opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
  761. return;
  762. }
  763. _opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
  764. await Task.Delay(retryDelay);
  765. retryDelay *= 2;
  766. }
  767. }
  768. }
  769. protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);
  770. private IQueryable<AdminLog> GetAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
  771. {
  772. // Save me from SQLite
  773. var query = StartAdminLogsQuery(db, filter);
  774. if (filter == null)
  775. {
  776. return query.OrderBy(log => log.Date);
  777. }
  778. if (filter.Round != null)
  779. {
  780. query = query.Where(log => log.RoundId == filter.Round);
  781. }
  782. if (filter.Types != null)
  783. {
  784. query = query.Where(log => filter.Types.Contains(log.Type));
  785. }
  786. if (filter.Impacts != null)
  787. {
  788. query = query.Where(log => filter.Impacts.Contains(log.Impact));
  789. }
  790. if (filter.Before != null)
  791. {
  792. query = query.Where(log => log.Date < filter.Before);
  793. }
  794. if (filter.After != null)
  795. {
  796. query = query.Where(log => log.Date > filter.After);
  797. }
  798. if (filter.IncludePlayers)
  799. {
  800. if (filter.AnyPlayers != null)
  801. {
  802. query = query.Where(log =>
  803. log.Players.Any(p => filter.AnyPlayers.Contains(p.PlayerUserId)) ||
  804. log.Players.Count == 0 && filter.IncludeNonPlayers);
  805. }
  806. if (filter.AllPlayers != null)
  807. {
  808. query = query.Where(log =>
  809. log.Players.All(p => filter.AllPlayers.Contains(p.PlayerUserId)) ||
  810. log.Players.Count == 0 && filter.IncludeNonPlayers);
  811. }
  812. }
  813. else
  814. {
  815. query = query.Where(log => log.Players.Count == 0);
  816. }
  817. if (filter.LastLogId != null)
  818. {
  819. query = filter.DateOrder switch
  820. {
  821. DateOrder.Ascending => query.Where(log => log.Id > filter.LastLogId),
  822. DateOrder.Descending => query.Where(log => log.Id < filter.LastLogId),
  823. _ => throw new ArgumentOutOfRangeException(nameof(filter),
  824. $"Unknown {nameof(DateOrder)} value {filter.DateOrder}")
  825. };
  826. }
  827. query = filter.DateOrder switch
  828. {
  829. DateOrder.Ascending => query.OrderBy(log => log.Date),
  830. DateOrder.Descending => query.OrderByDescending(log => log.Date),
  831. _ => throw new ArgumentOutOfRangeException(nameof(filter),
  832. $"Unknown {nameof(DateOrder)} value {filter.DateOrder}")
  833. };
  834. const int hardLogLimit = 500_000;
  835. if (filter.Limit != null)
  836. {
  837. query = query.Take(Math.Min(filter.Limit.Value, hardLogLimit));
  838. }
  839. else
  840. {
  841. query = query.Take(hardLogLimit);
  842. }
  843. return query;
  844. }
  845. public async IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
  846. {
  847. await using var db = await GetDb();
  848. var query = GetAdminLogsQuery(db.DbContext, filter);
  849. await foreach (var log in query.Select(log => log.Message).AsAsyncEnumerable())
  850. {
  851. yield return log;
  852. }
  853. }
  854. public async IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
  855. {
  856. await using var db = await GetDb();
  857. var query = GetAdminLogsQuery(db.DbContext, filter);
  858. query = query.Include(log => log.Players);
  859. await foreach (var log in query.AsAsyncEnumerable())
  860. {
  861. var players = new Guid[log.Players.Count];
  862. for (var i = 0; i < log.Players.Count; i++)
  863. {
  864. players[i] = log.Players[i].PlayerUserId;
  865. }
  866. yield return new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.Message, players);
  867. }
  868. }
  869. public async IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
  870. {
  871. await using var db = await GetDb();
  872. var query = GetAdminLogsQuery(db.DbContext, filter);
  873. await foreach (var json in query.Select(log => log.Json).AsAsyncEnumerable())
  874. {
  875. yield return json;
  876. }
  877. }
  878. public async Task<int> CountAdminLogs(int round)
  879. {
  880. await using var db = await GetDb();
  881. return await db.DbContext.AdminLog.CountAsync(log => log.RoundId == round);
  882. }
  883. #endregion
  884. #region Whitelist
  885. public async Task<bool> GetWhitelistStatusAsync(NetUserId player)
  886. {
  887. await using var db = await GetDb();
  888. return await db.DbContext.Whitelist.AnyAsync(w => w.UserId == player);
  889. }
  890. public async Task AddToWhitelistAsync(NetUserId player)
  891. {
  892. await using var db = await GetDb();
  893. db.DbContext.Whitelist.Add(new Whitelist { UserId = player });
  894. await db.DbContext.SaveChangesAsync();
  895. }
  896. public async Task RemoveFromWhitelistAsync(NetUserId player)
  897. {
  898. await using var db = await GetDb();
  899. var entry = await db.DbContext.Whitelist.SingleAsync(w => w.UserId == player);
  900. db.DbContext.Whitelist.Remove(entry);
  901. await db.DbContext.SaveChangesAsync();
  902. }
  903. public async Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
  904. {
  905. await using var db = await GetDb();
  906. return NormalizeDatabaseTime(await db.DbContext.Player
  907. .Where(dbPlayer => dbPlayer.UserId == player)
  908. .Select(dbPlayer => dbPlayer.LastReadRules)
  909. .SingleOrDefaultAsync());
  910. }
  911. public async Task SetLastReadRules(NetUserId player, DateTimeOffset? date)
  912. {
  913. await using var db = await GetDb();
  914. var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player).SingleOrDefaultAsync();
  915. if (dbPlayer == null)
  916. {
  917. return;
  918. }
  919. dbPlayer.LastReadRules = date?.UtcDateTime;
  920. await db.DbContext.SaveChangesAsync();
  921. }
  922. public async Task<bool> GetBlacklistStatusAsync(NetUserId player)
  923. {
  924. await using var db = await GetDb();
  925. return await db.DbContext.Blacklist.AnyAsync(w => w.UserId == player);
  926. }
  927. public async Task AddToBlacklistAsync(NetUserId player)
  928. {
  929. await using var db = await GetDb();
  930. db.DbContext.Blacklist.Add(new Blacklist() { UserId = player });
  931. await db.DbContext.SaveChangesAsync();
  932. }
  933. public async Task RemoveFromBlacklistAsync(NetUserId player)
  934. {
  935. await using var db = await GetDb();
  936. var entry = await db.DbContext.Blacklist.SingleAsync(w => w.UserId == player);
  937. db.DbContext.Blacklist.Remove(entry);
  938. await db.DbContext.SaveChangesAsync();
  939. }
  940. #endregion
  941. #region Uploaded Resources Logs
  942. public async Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
  943. {
  944. await using var db = await GetDb();
  945. db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date.UtcDateTime, Path = path, Data = data });
  946. await db.DbContext.SaveChangesAsync();
  947. }
  948. public async Task PurgeUploadedResourceLogAsync(int days)
  949. {
  950. await using var db = await GetDb();
  951. var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(days));
  952. await foreach (var log in db.DbContext.UploadedResourceLog
  953. .Where(l => date > l.Date)
  954. .AsAsyncEnumerable())
  955. {
  956. db.DbContext.UploadedResourceLog.Remove(log);
  957. }
  958. await db.DbContext.SaveChangesAsync();
  959. }
  960. #endregion
  961. #region Admin Notes
  962. public virtual async Task<int> AddAdminNote(AdminNote note)
  963. {
  964. await using var db = await GetDb();
  965. db.DbContext.AdminNotes.Add(note);
  966. await db.DbContext.SaveChangesAsync();
  967. return note.Id;
  968. }
  969. public virtual async Task<int> AddAdminWatchlist(AdminWatchlist watchlist)
  970. {
  971. await using var db = await GetDb();
  972. db.DbContext.AdminWatchlists.Add(watchlist);
  973. await db.DbContext.SaveChangesAsync();
  974. return watchlist.Id;
  975. }
  976. public virtual async Task<int> AddAdminMessage(AdminMessage message)
  977. {
  978. await using var db = await GetDb();
  979. db.DbContext.AdminMessages.Add(message);
  980. await db.DbContext.SaveChangesAsync();
  981. return message.Id;
  982. }
  983. public async Task<AdminNoteRecord?> GetAdminNote(int id)
  984. {
  985. await using var db = await GetDb();
  986. var entity = await db.DbContext.AdminNotes
  987. .Where(note => note.Id == id)
  988. .Include(note => note.Round)
  989. .ThenInclude(r => r!.Server)
  990. .Include(note => note.CreatedBy)
  991. .Include(note => note.LastEditedBy)
  992. .Include(note => note.DeletedBy)
  993. .Include(note => note.Player)
  994. .SingleOrDefaultAsync();
  995. return entity == null ? null : MakeAdminNoteRecord(entity);
  996. }
  997. private AdminNoteRecord MakeAdminNoteRecord(AdminNote entity)
  998. {
  999. return new AdminNoteRecord(
  1000. entity.Id,
  1001. MakeRoundRecord(entity.Round),
  1002. MakePlayerRecord(entity.Player),
  1003. entity.PlaytimeAtNote,
  1004. entity.Message,
  1005. entity.Severity,
  1006. MakePlayerRecord(entity.CreatedBy),
  1007. NormalizeDatabaseTime(entity.CreatedAt),
  1008. MakePlayerRecord(entity.LastEditedBy),
  1009. NormalizeDatabaseTime(entity.LastEditedAt),
  1010. NormalizeDatabaseTime(entity.ExpirationTime),
  1011. entity.Deleted,
  1012. MakePlayerRecord(entity.DeletedBy),
  1013. NormalizeDatabaseTime(entity.DeletedAt),
  1014. entity.Secret);
  1015. }
  1016. public async Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
  1017. {
  1018. await using var db = await GetDb();
  1019. var entity = await db.DbContext.AdminWatchlists
  1020. .Where(note => note.Id == id)
  1021. .Include(note => note.Round)
  1022. .ThenInclude(r => r!.Server)
  1023. .Include(note => note.CreatedBy)
  1024. .Include(note => note.LastEditedBy)
  1025. .Include(note => note.DeletedBy)
  1026. .Include(note => note.Player)
  1027. .SingleOrDefaultAsync();
  1028. return entity == null ? null : MakeAdminWatchlistRecord(entity);
  1029. }
  1030. public async Task<AdminMessageRecord?> GetAdminMessage(int id)
  1031. {
  1032. await using var db = await GetDb();
  1033. var entity = await db.DbContext.AdminMessages
  1034. .Where(note => note.Id == id)
  1035. .Include(note => note.Round)
  1036. .ThenInclude(r => r!.Server)
  1037. .Include(note => note.CreatedBy)
  1038. .Include(note => note.LastEditedBy)
  1039. .Include(note => note.DeletedBy)
  1040. .Include(note => note.Player)
  1041. .SingleOrDefaultAsync();
  1042. return entity == null ? null : MakeAdminMessageRecord(entity);
  1043. }
  1044. private AdminMessageRecord MakeAdminMessageRecord(AdminMessage entity)
  1045. {
  1046. return new AdminMessageRecord(
  1047. entity.Id,
  1048. MakeRoundRecord(entity.Round),
  1049. MakePlayerRecord(entity.Player),
  1050. entity.PlaytimeAtNote,
  1051. entity.Message,
  1052. MakePlayerRecord(entity.CreatedBy),
  1053. NormalizeDatabaseTime(entity.CreatedAt),
  1054. MakePlayerRecord(entity.LastEditedBy),
  1055. NormalizeDatabaseTime(entity.LastEditedAt),
  1056. NormalizeDatabaseTime(entity.ExpirationTime),
  1057. entity.Deleted,
  1058. MakePlayerRecord(entity.DeletedBy),
  1059. NormalizeDatabaseTime(entity.DeletedAt),
  1060. entity.Seen,
  1061. entity.Dismissed);
  1062. }
  1063. public async Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
  1064. {
  1065. await using var db = await GetDb();
  1066. var ban = await db.DbContext.Ban
  1067. .Include(ban => ban.Unban)
  1068. .Include(ban => ban.Round)
  1069. .ThenInclude(r => r!.Server)
  1070. .Include(ban => ban.CreatedBy)
  1071. .Include(ban => ban.LastEditedBy)
  1072. .Include(ban => ban.Unban)
  1073. .SingleOrDefaultAsync(b => b.Id == id);
  1074. if (ban is null)
  1075. return null;
  1076. var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
  1077. return new ServerBanNoteRecord(
  1078. ban.Id,
  1079. MakeRoundRecord(ban.Round),
  1080. MakePlayerRecord(player),
  1081. ban.PlaytimeAtNote,
  1082. ban.Reason,
  1083. ban.Severity,
  1084. MakePlayerRecord(ban.CreatedBy),
  1085. ban.BanTime,
  1086. MakePlayerRecord(ban.LastEditedBy),
  1087. ban.LastEditedAt,
  1088. ban.ExpirationTime,
  1089. ban.Hidden,
  1090. MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
  1091. ? null
  1092. : await db.DbContext.Player.SingleOrDefaultAsync(p =>
  1093. p.UserId == ban.Unban.UnbanningAdmin.Value)),
  1094. ban.Unban?.UnbanTime);
  1095. }
  1096. public async Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
  1097. {
  1098. await using var db = await GetDb();
  1099. var ban = await db.DbContext.RoleBan
  1100. .Include(ban => ban.Unban)
  1101. .Include(ban => ban.Round)
  1102. .ThenInclude(r => r!.Server)
  1103. .Include(ban => ban.CreatedBy)
  1104. .Include(ban => ban.LastEditedBy)
  1105. .Include(ban => ban.Unban)
  1106. .SingleOrDefaultAsync(b => b.Id == id);
  1107. if (ban is null)
  1108. return null;
  1109. var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == ban.PlayerUserId);
  1110. var unbanningAdmin =
  1111. ban.Unban is null
  1112. ? null
  1113. : await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin);
  1114. return new ServerRoleBanNoteRecord(
  1115. ban.Id,
  1116. MakeRoundRecord(ban.Round),
  1117. MakePlayerRecord(player),
  1118. ban.PlaytimeAtNote,
  1119. ban.Reason,
  1120. ban.Severity,
  1121. MakePlayerRecord(ban.CreatedBy),
  1122. ban.BanTime,
  1123. MakePlayerRecord(ban.LastEditedBy),
  1124. ban.LastEditedAt,
  1125. ban.ExpirationTime,
  1126. ban.Hidden,
  1127. new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
  1128. MakePlayerRecord(unbanningAdmin),
  1129. ban.Unban?.UnbanTime);
  1130. }
  1131. public async Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
  1132. {
  1133. await using var db = await GetDb();
  1134. List<IAdminRemarksRecord> notes = new();
  1135. notes.AddRange(
  1136. (await (from note in db.DbContext.AdminNotes
  1137. where note.PlayerUserId == player &&
  1138. !note.Deleted &&
  1139. (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
  1140. select note)
  1141. .Include(note => note.Round)
  1142. .ThenInclude(r => r!.Server)
  1143. .Include(note => note.CreatedBy)
  1144. .Include(note => note.LastEditedBy)
  1145. .Include(note => note.Player)
  1146. .ToListAsync()).Select(MakeAdminNoteRecord));
  1147. notes.AddRange(await GetActiveWatchlistsImpl(db, player));
  1148. notes.AddRange(await GetMessagesImpl(db, player));
  1149. notes.AddRange(await GetServerBansAsNotesForUser(db, player));
  1150. notes.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
  1151. return notes;
  1152. }
  1153. public async Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
  1154. {
  1155. await using var db = await GetDb();
  1156. var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
  1157. note.Message = message;
  1158. note.Severity = severity;
  1159. note.Secret = secret;
  1160. note.LastEditedById = editedBy;
  1161. note.LastEditedAt = editedAt.UtcDateTime;
  1162. note.ExpirationTime = expiryTime?.UtcDateTime;
  1163. await db.DbContext.SaveChangesAsync();
  1164. }
  1165. public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
  1166. {
  1167. await using var db = await GetDb();
  1168. var note = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync();
  1169. note.Message = message;
  1170. note.LastEditedById = editedBy;
  1171. note.LastEditedAt = editedAt.UtcDateTime;
  1172. note.ExpirationTime = expiryTime?.UtcDateTime;
  1173. await db.DbContext.SaveChangesAsync();
  1174. }
  1175. public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
  1176. {
  1177. await using var db = await GetDb();
  1178. var note = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync();
  1179. note.Message = message;
  1180. note.LastEditedById = editedBy;
  1181. note.LastEditedAt = editedAt.UtcDateTime;
  1182. note.ExpirationTime = expiryTime?.UtcDateTime;
  1183. await db.DbContext.SaveChangesAsync();
  1184. }
  1185. public async Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt)
  1186. {
  1187. await using var db = await GetDb();
  1188. var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
  1189. note.Deleted = true;
  1190. note.DeletedById = deletedBy;
  1191. note.DeletedAt = deletedAt.UtcDateTime;
  1192. await db.DbContext.SaveChangesAsync();
  1193. }
  1194. public async Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt)
  1195. {
  1196. await using var db = await GetDb();
  1197. var watchlist = await db.DbContext.AdminWatchlists.Where(note => note.Id == id).SingleAsync();
  1198. watchlist.Deleted = true;
  1199. watchlist.DeletedById = deletedBy;
  1200. watchlist.DeletedAt = deletedAt.UtcDateTime;
  1201. await db.DbContext.SaveChangesAsync();
  1202. }
  1203. public async Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt)
  1204. {
  1205. await using var db = await GetDb();
  1206. var message = await db.DbContext.AdminMessages.Where(note => note.Id == id).SingleAsync();
  1207. message.Deleted = true;
  1208. message.DeletedById = deletedBy;
  1209. message.DeletedAt = deletedAt.UtcDateTime;
  1210. await db.DbContext.SaveChangesAsync();
  1211. }
  1212. public async Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
  1213. {
  1214. await using var db = await GetDb();
  1215. var ban = await db.DbContext.Ban.Where(ban => ban.Id == id).SingleAsync();
  1216. ban.Hidden = true;
  1217. ban.LastEditedById = deletedBy;
  1218. ban.LastEditedAt = deletedAt.UtcDateTime;
  1219. await db.DbContext.SaveChangesAsync();
  1220. }
  1221. public async Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
  1222. {
  1223. await using var db = await GetDb();
  1224. var roleBan = await db.DbContext.RoleBan.Where(roleBan => roleBan.Id == id).SingleAsync();
  1225. roleBan.Hidden = true;
  1226. roleBan.LastEditedById = deletedBy;
  1227. roleBan.LastEditedAt = deletedAt.UtcDateTime;
  1228. await db.DbContext.SaveChangesAsync();
  1229. }
  1230. public async Task<List<IAdminRemarksRecord>> GetVisibleAdminRemarks(Guid player)
  1231. {
  1232. await using var db = await GetDb();
  1233. List<IAdminRemarksRecord> notesCol = new();
  1234. notesCol.AddRange(
  1235. (await (from note in db.DbContext.AdminNotes
  1236. where note.PlayerUserId == player &&
  1237. !note.Secret &&
  1238. !note.Deleted &&
  1239. (note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
  1240. select note)
  1241. .Include(note => note.Round)
  1242. .ThenInclude(r => r!.Server)
  1243. .Include(note => note.CreatedBy)
  1244. .Include(note => note.Player)
  1245. .ToListAsync()).Select(MakeAdminNoteRecord));
  1246. notesCol.AddRange(await GetMessagesImpl(db, player));
  1247. notesCol.AddRange(await GetServerBansAsNotesForUser(db, player));
  1248. notesCol.AddRange(await GetGroupedServerRoleBansAsNotesForUser(db, player));
  1249. return notesCol;
  1250. }
  1251. public async Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
  1252. {
  1253. await using var db = await GetDb();
  1254. return await GetActiveWatchlistsImpl(db, player);
  1255. }
  1256. protected async Task<List<AdminWatchlistRecord>> GetActiveWatchlistsImpl(DbGuard db, Guid player)
  1257. {
  1258. var entities = await (from watchlist in db.DbContext.AdminWatchlists
  1259. where watchlist.PlayerUserId == player &&
  1260. !watchlist.Deleted &&
  1261. (watchlist.ExpirationTime == null || DateTime.UtcNow < watchlist.ExpirationTime)
  1262. select watchlist)
  1263. .Include(note => note.Round)
  1264. .ThenInclude(r => r!.Server)
  1265. .Include(note => note.CreatedBy)
  1266. .Include(note => note.LastEditedBy)
  1267. .Include(note => note.Player)
  1268. .ToListAsync();
  1269. return entities.Select(MakeAdminWatchlistRecord).ToList();
  1270. }
  1271. private AdminWatchlistRecord MakeAdminWatchlistRecord(AdminWatchlist entity)
  1272. {
  1273. 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));
  1274. }
  1275. public async Task<List<AdminMessageRecord>> GetMessages(Guid player)
  1276. {
  1277. await using var db = await GetDb();
  1278. return await GetMessagesImpl(db, player);
  1279. }
  1280. protected async Task<List<AdminMessageRecord>> GetMessagesImpl(DbGuard db, Guid player)
  1281. {
  1282. var entities = await (from message in db.DbContext.AdminMessages
  1283. where message.PlayerUserId == player && !message.Deleted &&
  1284. (message.ExpirationTime == null || DateTime.UtcNow < message.ExpirationTime)
  1285. select message).Include(note => note.Round)
  1286. .ThenInclude(r => r!.Server)
  1287. .Include(note => note.CreatedBy)
  1288. .Include(note => note.LastEditedBy)
  1289. .Include(note => note.Player)
  1290. .ToListAsync();
  1291. return entities.Select(MakeAdminMessageRecord).ToList();
  1292. }
  1293. public async Task MarkMessageAsSeen(int id, bool dismissedToo)
  1294. {
  1295. await using var db = await GetDb();
  1296. var message = await db.DbContext.AdminMessages.SingleAsync(m => m.Id == id);
  1297. message.Seen = true;
  1298. if (dismissedToo)
  1299. message.Dismissed = true;
  1300. await db.DbContext.SaveChangesAsync();
  1301. }
  1302. // These two are here because they get converted into notes later
  1303. protected async Task<List<ServerBanNoteRecord>> GetServerBansAsNotesForUser(DbGuard db, Guid user)
  1304. {
  1305. // You can't group queries, as player will not always exist. When it doesn't, the
  1306. // whole query returns nothing
  1307. var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
  1308. var bans = await db.DbContext.Ban
  1309. .Where(ban => ban.PlayerUserId == user && !ban.Hidden)
  1310. .Include(ban => ban.Unban)
  1311. .Include(ban => ban.Round)
  1312. .ThenInclude(r => r!.Server)
  1313. .Include(ban => ban.CreatedBy)
  1314. .Include(ban => ban.LastEditedBy)
  1315. .Include(ban => ban.Unban)
  1316. .ToArrayAsync();
  1317. var banNotes = new List<ServerBanNoteRecord>();
  1318. foreach (var ban in bans)
  1319. {
  1320. var banNote = new ServerBanNoteRecord(
  1321. ban.Id,
  1322. MakeRoundRecord(ban.Round),
  1323. MakePlayerRecord(player),
  1324. ban.PlaytimeAtNote,
  1325. ban.Reason,
  1326. ban.Severity,
  1327. MakePlayerRecord(ban.CreatedBy),
  1328. NormalizeDatabaseTime(ban.BanTime),
  1329. MakePlayerRecord(ban.LastEditedBy),
  1330. NormalizeDatabaseTime(ban.LastEditedAt),
  1331. NormalizeDatabaseTime(ban.ExpirationTime),
  1332. ban.Hidden,
  1333. MakePlayerRecord(ban.Unban?.UnbanningAdmin == null
  1334. ? null
  1335. : await db.DbContext.Player.SingleOrDefaultAsync(
  1336. p => p.UserId == ban.Unban.UnbanningAdmin.Value)),
  1337. NormalizeDatabaseTime(ban.Unban?.UnbanTime));
  1338. banNotes.Add(banNote);
  1339. }
  1340. return banNotes;
  1341. }
  1342. protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
  1343. {
  1344. // Server side query
  1345. var bansQuery = await db.DbContext.RoleBan
  1346. .Where(ban => ban.PlayerUserId == user && !ban.Hidden)
  1347. .Include(ban => ban.Unban)
  1348. .Include(ban => ban.Round)
  1349. .ThenInclude(r => r!.Server)
  1350. .Include(ban => ban.CreatedBy)
  1351. .Include(ban => ban.LastEditedBy)
  1352. .Include(ban => ban.Unban)
  1353. .ToArrayAsync();
  1354. // Client side query, as EF can't do groups yet
  1355. var bansEnumerable = bansQuery
  1356. .GroupBy(ban => new { ban.BanTime, CreatedBy = (Player?)ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null })
  1357. .Select(banGroup => banGroup)
  1358. .ToArray();
  1359. List<ServerRoleBanNoteRecord> bans = new();
  1360. var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
  1361. foreach (var banGroup in bansEnumerable)
  1362. {
  1363. var firstBan = banGroup.First();
  1364. Player? unbanningAdmin = null;
  1365. if (firstBan.Unban?.UnbanningAdmin is not null)
  1366. unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value);
  1367. bans.Add(new ServerRoleBanNoteRecord(
  1368. firstBan.Id,
  1369. MakeRoundRecord(firstBan.Round),
  1370. MakePlayerRecord(player),
  1371. firstBan.PlaytimeAtNote,
  1372. firstBan.Reason,
  1373. firstBan.Severity,
  1374. MakePlayerRecord(firstBan.CreatedBy),
  1375. NormalizeDatabaseTime(firstBan.BanTime),
  1376. MakePlayerRecord(firstBan.LastEditedBy),
  1377. NormalizeDatabaseTime(firstBan.LastEditedAt),
  1378. NormalizeDatabaseTime(firstBan.ExpirationTime),
  1379. firstBan.Hidden,
  1380. banGroup.Select(ban => ban.RoleId.Replace(BanManager.JobPrefix, null)).ToArray(),
  1381. MakePlayerRecord(unbanningAdmin),
  1382. NormalizeDatabaseTime(firstBan.Unban?.UnbanTime)));
  1383. }
  1384. return bans;
  1385. }
  1386. #endregion
  1387. #region Job Whitelists
  1388. public async Task<bool> AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
  1389. {
  1390. await using var db = await GetDb();
  1391. var exists = await db.DbContext.RoleWhitelists
  1392. .Where(w => w.PlayerUserId == player)
  1393. .Where(w => w.RoleId == job.Id)
  1394. .AnyAsync();
  1395. if (exists)
  1396. return false;
  1397. var whitelist = new RoleWhitelist
  1398. {
  1399. PlayerUserId = player,
  1400. RoleId = job
  1401. };
  1402. db.DbContext.RoleWhitelists.Add(whitelist);
  1403. await db.DbContext.SaveChangesAsync();
  1404. return true;
  1405. }
  1406. public async Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel)
  1407. {
  1408. await using var db = await GetDb(cancel);
  1409. return await db.DbContext.RoleWhitelists
  1410. .Where(w => w.PlayerUserId == player)
  1411. .Select(w => w.RoleId)
  1412. .ToListAsync(cancellationToken: cancel);
  1413. }
  1414. public async Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
  1415. {
  1416. await using var db = await GetDb();
  1417. return await db.DbContext.RoleWhitelists
  1418. .Where(w => w.PlayerUserId == player)
  1419. .Where(w => w.RoleId == job.Id)
  1420. .AnyAsync();
  1421. }
  1422. public async Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
  1423. {
  1424. await using var db = await GetDb();
  1425. var entry = await db.DbContext.RoleWhitelists
  1426. .Where(w => w.PlayerUserId == player)
  1427. .Where(w => w.RoleId == job.Id)
  1428. .SingleOrDefaultAsync();
  1429. if (entry == null)
  1430. return false;
  1431. db.DbContext.RoleWhitelists.Remove(entry);
  1432. await db.DbContext.SaveChangesAsync();
  1433. return true;
  1434. }
  1435. #endregion
  1436. # region IPIntel
  1437. public async Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
  1438. {
  1439. while (true)
  1440. {
  1441. try
  1442. {
  1443. await using var db = await GetDb();
  1444. var existing = await db.DbContext.IPIntelCache
  1445. .Where(w => ip.Equals(w.Address))
  1446. .SingleOrDefaultAsync();
  1447. if (existing == null)
  1448. {
  1449. var newCache = new IPIntelCache
  1450. {
  1451. Time = time,
  1452. Address = ip,
  1453. Score = score,
  1454. };
  1455. db.DbContext.IPIntelCache.Add(newCache);
  1456. }
  1457. else
  1458. {
  1459. existing.Time = time;
  1460. existing.Score = score;
  1461. }
  1462. await Task.Delay(5000);
  1463. await db.DbContext.SaveChangesAsync();
  1464. return true;
  1465. }
  1466. catch (DbUpdateException)
  1467. {
  1468. _opsLog.Warning("IPIntel UPSERT failed with a db exception... retrying.");
  1469. }
  1470. }
  1471. }
  1472. public async Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
  1473. {
  1474. await using var db = await GetDb();
  1475. return await db.DbContext.IPIntelCache
  1476. .SingleOrDefaultAsync(w => ip.Equals(w.Address));
  1477. }
  1478. public async Task<bool> CleanIPIntelCache(TimeSpan range)
  1479. {
  1480. await using var db = await GetDb();
  1481. // Calculating this here cause otherwise sqlite whines.
  1482. var cutoffTime = DateTime.UtcNow.Subtract(range);
  1483. await db.DbContext.IPIntelCache
  1484. .Where(w => w.Time <= cutoffTime)
  1485. .ExecuteDeleteAsync();
  1486. await db.DbContext.SaveChangesAsync();
  1487. return true;
  1488. }
  1489. #endregion
  1490. public abstract Task SendNotification(DatabaseNotification notification);
  1491. // SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
  1492. // Normalize DateTimes here so they're always Utc. Thanks.
  1493. protected abstract DateTime NormalizeDatabaseTime(DateTime time);
  1494. [return: NotNullIfNotNull(nameof(time))]
  1495. protected DateTime? NormalizeDatabaseTime(DateTime? time)
  1496. {
  1497. return time != null ? NormalizeDatabaseTime(time.Value) : time;
  1498. }
  1499. public async Task<bool> HasPendingModelChanges()
  1500. {
  1501. await using var db = await GetDb();
  1502. return db.DbContext.Database.HasPendingModelChanges();
  1503. }
  1504. protected abstract Task<DbGuard> GetDb(
  1505. CancellationToken cancel = default,
  1506. [CallerMemberName] string? name = null);
  1507. protected void LogDbOp(string? name)
  1508. {
  1509. _opsLog.Verbose($"Running DB operation: {name ?? "unknown"}");
  1510. }
  1511. protected abstract class DbGuard : IAsyncDisposable
  1512. {
  1513. public abstract ServerDbContext DbContext { get; }
  1514. public abstract ValueTask DisposeAsync();
  1515. }
  1516. protected void NotificationReceived(DatabaseNotification notification)
  1517. {
  1518. OnNotificationReceived?.Invoke(notification);
  1519. }
  1520. public virtual void Shutdown()
  1521. {
  1522. }
  1523. }
  1524. }