Update actions/attest-build-provenance action to v1.3.1
[ArchiSteamFarm.git] / ArchiSteamFarm / Storage / GlobalDatabase.cs
blob42fe3e10b6061969c8d726113d546b6c5e802bd3
1 // ----------------------------------------------------------------------------------------------
2 // _ _ _ ____ _ _____
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
8 // |
9 // Copyright 2015-2024 Ɓukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
11 // |
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
15 // |
16 // http://www.apache.org/licenses/LICENSE-2.0
17 // |
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
24 using System;
25 using System.Collections.Concurrent;
26 using System.Collections.Generic;
27 using System.Globalization;
28 using System.IO;
29 using System.Linq;
30 using System.Text.Json;
31 using System.Text.Json.Serialization;
32 using System.Threading;
33 using System.Threading.Tasks;
34 using ArchiSteamFarm.Collections;
35 using ArchiSteamFarm.Core;
36 using ArchiSteamFarm.Helpers.Json;
37 using ArchiSteamFarm.Localization;
38 using ArchiSteamFarm.Steam;
39 using ArchiSteamFarm.Steam.SteamKit2;
40 using JetBrains.Annotations;
42 namespace ArchiSteamFarm.Storage;
44 public sealed class GlobalDatabase : GenericDatabase {
45 [JsonIgnore]
46 [PublicAPI]
47 public IReadOnlyDictionary<uint, ulong> PackageAccessTokensReadOnly => PackagesAccessTokens;
49 [JsonIgnore]
50 [PublicAPI]
51 public IReadOnlyDictionary<uint, PackageData> PackagesDataReadOnly => PackagesData;
53 private readonly SemaphoreSlim PackagesRefreshSemaphore = new(1, 1);
55 [JsonInclude]
56 [PublicAPI]
57 public Guid Identifier { get; private init; } = Guid.NewGuid();
59 [JsonDisallowNull]
60 [JsonInclude]
61 internal ConcurrentHashSet<ulong> CachedBadBots { get; private init; } = [];
63 [JsonDisallowNull]
64 [JsonInclude]
65 internal ObservableConcurrentDictionary<uint, byte> CardCountsPerGame { get; private init; } = new();
67 internal uint CellID {
68 get => BackingCellID;
70 set {
71 if (BackingCellID == value) {
72 return;
75 BackingCellID = value;
76 Utilities.InBackground(Save);
80 internal uint LastChangeNumber {
81 get => BackingLastChangeNumber;
83 set {
84 if (BackingLastChangeNumber == value) {
85 return;
88 BackingLastChangeNumber = value;
89 Utilities.InBackground(Save);
93 [JsonDisallowNull]
94 [JsonInclude]
95 internal InMemoryServerListProvider ServerListProvider { get; private init; } = new();
97 [JsonInclude]
98 [JsonPropertyName($"_{nameof(CellID)}")]
99 private uint BackingCellID { get; set; }
101 [JsonInclude]
102 [JsonPropertyName($"_{nameof(LastChangeNumber)}")]
103 private uint BackingLastChangeNumber { get; set; }
105 [JsonDisallowNull]
106 [JsonInclude]
107 private ConcurrentDictionary<uint, ulong> PackagesAccessTokens { get; init; } = new();
109 [JsonDisallowNull]
110 [JsonInclude]
111 private ConcurrentDictionary<uint, PackageData> PackagesData { get; init; } = new();
113 private GlobalDatabase(string filePath) : this() {
114 ArgumentException.ThrowIfNullOrEmpty(filePath);
116 FilePath = filePath;
119 [JsonConstructor]
120 private GlobalDatabase() {
121 CachedBadBots.OnModified += OnObjectModified;
122 CardCountsPerGame.OnModified += OnObjectModified;
123 ServerListProvider.ServerListUpdated += OnObjectModified;
126 [PublicAPI]
127 public void DeleteFromJsonStorage(string key) {
128 ArgumentException.ThrowIfNullOrEmpty(key);
130 DeleteFromJsonStorage(this, key);
133 [PublicAPI]
134 public void SaveToJsonStorage<T>(string key, T value) where T : notnull {
135 ArgumentException.ThrowIfNullOrEmpty(key);
136 ArgumentNullException.ThrowIfNull(value);
138 SaveToJsonStorage(this, key, value);
141 [PublicAPI]
142 public void SaveToJsonStorage(string key, JsonElement value) {
143 ArgumentException.ThrowIfNullOrEmpty(key);
145 if (value.ValueKind == JsonValueKind.Undefined) {
146 throw new ArgumentOutOfRangeException(nameof(value));
149 SaveToJsonStorage(this, key, value);
152 [UsedImplicitly]
153 public bool ShouldSerializeBackingCellID() => BackingCellID != 0;
155 [UsedImplicitly]
156 public bool ShouldSerializeBackingLastChangeNumber() => BackingLastChangeNumber != 0;
158 [UsedImplicitly]
159 public bool ShouldSerializeCachedBadBots() => CachedBadBots.Count > 0;
161 [UsedImplicitly]
162 public bool ShouldSerializeCardCountsPerGame() => !CardCountsPerGame.IsEmpty;
164 [UsedImplicitly]
165 public bool ShouldSerializePackagesAccessTokens() => !PackagesAccessTokens.IsEmpty;
167 [UsedImplicitly]
168 public bool ShouldSerializePackagesData() => !PackagesData.IsEmpty;
170 [UsedImplicitly]
171 public bool ShouldSerializeServerListProvider() => ServerListProvider.ShouldSerializeServerRecords();
173 protected override void Dispose(bool disposing) {
174 if (disposing) {
175 // Events we registered
176 CachedBadBots.OnModified -= OnObjectModified;
177 CardCountsPerGame.OnModified -= OnObjectModified;
178 ServerListProvider.ServerListUpdated -= OnObjectModified;
180 // Those are objects that are always being created if constructor doesn't throw exception
181 PackagesRefreshSemaphore.Dispose();
184 // Base dispose
185 base.Dispose(disposing);
188 protected override Task Save() => Save(this);
190 internal static async Task<GlobalDatabase?> CreateOrLoad(string filePath) {
191 ArgumentException.ThrowIfNullOrEmpty(filePath);
193 if (!File.Exists(filePath)) {
194 GlobalDatabase result = new(filePath);
196 Utilities.InBackground(() => Save(result));
198 return result;
201 GlobalDatabase? globalDatabase;
203 try {
204 string json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
206 if (string.IsNullOrEmpty(json)) {
207 ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
209 return null;
212 globalDatabase = json.ToJsonObject<GlobalDatabase>();
213 } catch (Exception e) {
214 ASF.ArchiLogger.LogGenericException(e);
216 return null;
219 if (globalDatabase == null) {
220 ASF.ArchiLogger.LogNullError(globalDatabase);
222 return null;
225 globalDatabase.FilePath = filePath;
227 return globalDatabase;
230 internal HashSet<uint> GetPackageIDs(uint appID, IEnumerable<uint> packageIDs, int limit = int.MaxValue) {
231 ArgumentOutOfRangeException.ThrowIfZero(appID);
232 ArgumentNullException.ThrowIfNull(packageIDs);
234 HashSet<uint> result = [];
236 foreach (uint packageID in packageIDs.Where(static packageID => packageID != 0)) {
237 if (!PackagesData.TryGetValue(packageID, out PackageData? packageEntry) || (packageEntry.AppIDs?.Contains(appID) != true)) {
238 continue;
241 result.Add(packageID);
243 if (result.Count >= limit) {
244 return result;
248 return result;
251 internal async Task OnPICSChangesRestart(uint currentChangeNumber) {
252 ArgumentOutOfRangeException.ThrowIfZero(currentChangeNumber);
254 if (Bot.Bots == null) {
255 throw new InvalidOperationException(nameof(Bot.Bots));
258 if (currentChangeNumber <= LastChangeNumber) {
259 return;
262 LastChangeNumber = currentChangeNumber;
264 Bot? refreshBot = Bot.Bots.Values.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
266 if (refreshBot == null) {
267 return;
270 if (PackagesData.IsEmpty) {
271 return;
274 Dictionary<uint, uint> packageIDs = PackagesData.Keys.ToDictionary(static packageID => packageID, _ => currentChangeNumber);
276 await RefreshPackages(refreshBot, packageIDs).ConfigureAwait(false);
279 internal void RefreshPackageAccessTokens(IReadOnlyDictionary<uint, ulong> packageAccessTokens) {
280 if ((packageAccessTokens == null) || (packageAccessTokens.Count == 0)) {
281 throw new ArgumentNullException(nameof(packageAccessTokens));
284 bool save = false;
286 foreach ((uint packageID, ulong currentAccessToken) in packageAccessTokens) {
287 if (!PackagesAccessTokens.TryGetValue(packageID, out ulong previousAccessToken) || (previousAccessToken != currentAccessToken)) {
288 PackagesAccessTokens[packageID] = currentAccessToken;
289 save = true;
293 if (save) {
294 Utilities.InBackground(Save);
298 internal async Task RefreshPackages(Bot bot, IReadOnlyDictionary<uint, uint> packages) {
299 ArgumentNullException.ThrowIfNull(bot);
301 if ((packages == null) || (packages.Count == 0)) {
302 throw new ArgumentNullException(nameof(packages));
305 await PackagesRefreshSemaphore.WaitAsync().ConfigureAwait(false);
307 try {
308 DateTime now = DateTime.UtcNow;
310 HashSet<uint> packageIDs = packages.Where(package => (package.Key != 0) && (!PackagesData.TryGetValue(package.Key, out PackageData? previousData) || (previousData.ChangeNumber < package.Value) || (previousData.ValidUntil < now))).Select(static package => package.Key).ToHashSet();
312 if (packageIDs.Count == 0) {
313 return;
316 Dictionary<uint, PackageData>? packagesData = await bot.GetPackagesData(packageIDs).ConfigureAwait(false);
318 if (packagesData == null) {
319 bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
321 return;
324 foreach ((uint packageID, PackageData packageData) in packagesData) {
325 PackagesData[packageID] = packageData;
328 Utilities.InBackground(Save);
329 } finally {
330 PackagesRefreshSemaphore.Release();
334 private async void OnObjectModified(object? sender, EventArgs e) {
335 if (string.IsNullOrEmpty(FilePath)) {
336 return;
339 await Save().ConfigureAwait(false);