1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2025 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
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
16 // http://www.apache.org/licenses/LICENSE-2.0
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.
25 using System
.Collections
.Concurrent
;
26 using System
.Collections
.Frozen
;
27 using System
.Collections
.Generic
;
28 using System
.ComponentModel
;
29 using System
.Composition
;
32 using System
.Text
.Json
;
33 using System
.Text
.Json
.Serialization
;
34 using System
.Threading
;
35 using System
.Threading
.Tasks
;
36 using ArchiSteamFarm
.Core
;
37 using ArchiSteamFarm
.Helpers
;
38 using ArchiSteamFarm
.Helpers
.Json
;
39 using ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
.Data
;
40 using ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
.Localization
;
41 using ArchiSteamFarm
.Plugins
;
42 using ArchiSteamFarm
.Plugins
.Interfaces
;
43 using ArchiSteamFarm
.Steam
;
44 using ArchiSteamFarm
.Steam
.Interaction
;
45 using ArchiSteamFarm
.Storage
;
46 using ArchiSteamFarm
.Web
;
47 using ArchiSteamFarm
.Web
.Responses
;
50 namespace ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
;
52 [Export(typeof(IPlugin
))]
53 internal sealed class SteamTokenDumperPlugin
: OfficialPlugin
, IASF
, IBot
, IBotCommand2
, IBotSteamClient
, ISteamPICSChanges
{
54 private const ushort DepotsRateLimitingDelay
= 500;
56 internal static SteamTokenDumperConfig
? Config { get; private set; }
58 private static readonly ConcurrentDictionary
<Bot
, IDisposable
> BotSubscriptions
= new();
59 private static readonly ConcurrentDictionary
<Bot
, (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
)> BotSynchronizations
= new();
60 private static readonly SemaphoreSlim SubmissionSemaphore
= new(1, 1);
61 private static readonly Timer SubmissionTimer
= new(OnSubmissionTimer
);
63 private static GlobalCache
? GlobalCache
;
64 private static DateTimeOffset LastUploadAt
= DateTimeOffset
.MinValue
;
67 public override string Name
=> nameof(SteamTokenDumperPlugin
);
70 public override Version Version
=> typeof(SteamTokenDumperPlugin
).Assembly
.GetName().Version
?? throw new InvalidOperationException(nameof(Version
));
72 public Task
<uint> GetPreferredChangeNumberToStartFrom() => Task
.FromResult(GlobalCache
?.LastChangeNumber
?? 0);
74 public async Task
OnASFInit(IReadOnlyDictionary
<string, JsonElement
>? additionalConfigProperties
= null) {
75 if (!SharedInfo
.HasValidToken
) {
76 ASF
.ArchiLogger
.LogGenericError(Strings
.FormatPluginDisabledMissingBuildToken(nameof(SteamTokenDumperPlugin
)));
81 bool isEnabled
= false;
82 SteamTokenDumperConfig
? config
= null;
84 if (additionalConfigProperties
!= null) {
85 foreach ((string configProperty
, JsonElement configValue
) in additionalConfigProperties
) {
87 switch (configProperty
) {
88 case nameof(GlobalConfigExtension
.SteamTokenDumperPlugin
):
89 config
= configValue
.ToJsonObject
<SteamTokenDumperConfig
>();
92 case nameof(GlobalConfigExtension
.SteamTokenDumperPluginEnabled
) when configValue
.ValueKind
== JsonValueKind
.False
:
96 case nameof(GlobalConfigExtension
.SteamTokenDumperPluginEnabled
) when configValue
.ValueKind
== JsonValueKind
.True
:
101 } catch (Exception e
) {
102 ASF
.ArchiLogger
.LogGenericException(e
);
103 ASF
.ArchiLogger
.LogGenericWarning(Strings
.FormatPluginDisabledInConfig(nameof(SteamTokenDumperPlugin
)));
110 if (GlobalCache
== null) {
111 GlobalCache
? globalCache
= await GlobalCache
.Load().ConfigureAwait(false);
113 if (globalCache
== null) {
114 ASF
.ArchiLogger
.LogGenericError(Strings
.FormatFileCouldNotBeLoadedFreshInit(nameof(GlobalCache
)));
116 GlobalCache
= new GlobalCache();
118 GlobalCache
= globalCache
;
122 if (!isEnabled
&& (config
== null)) {
123 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatPluginDisabledInConfig(nameof(SteamTokenDumperPlugin
)));
128 config
??= new SteamTokenDumperConfig();
131 config
.Enabled
= true;
134 if (!config
.Enabled
) {
135 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatPluginDisabledInConfig(nameof(SteamTokenDumperPlugin
)));
138 if (!config
.SecretAppIDs
.IsEmpty
) {
139 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatPluginSecretListInitialized(nameof(config
.SecretAppIDs
), string.Join(", ", config
.SecretAppIDs
)));
142 if (!config
.SecretPackageIDs
.IsEmpty
) {
143 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatPluginSecretListInitialized(nameof(config
.SecretPackageIDs
), string.Join(", ", config
.SecretPackageIDs
)));
146 if (!config
.SecretDepotIDs
.IsEmpty
) {
147 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatPluginSecretListInitialized(nameof(config
.SecretDepotIDs
), string.Join(", ", config
.SecretDepotIDs
)));
152 if (!config
.Enabled
) {
156 #pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
157 TimeSpan startIn
= TimeSpan
.FromMinutes(Random
.Shared
.Next(SharedInfo
.MinimumMinutesBeforeFirstUpload
, SharedInfo
.MaximumMinutesBeforeFirstUpload
));
158 #pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
160 // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
161 lock (SubmissionSemaphore
) {
162 SubmissionTimer
.Change(startIn
, TimeSpan
.FromHours(SharedInfo
.HoursBetweenUploads
));
165 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatPluginInitializedAndEnabled(nameof(SteamTokenDumperPlugin
), startIn
.ToHumanReadable()));
168 public Task
<string?> OnBotCommand(Bot bot
, EAccess access
, string message
, string[] args
, ulong steamID
= 0) {
169 ArgumentNullException
.ThrowIfNull(bot
);
171 if (!Enum
.IsDefined(access
)) {
172 throw new InvalidEnumArgumentException(nameof(access
), (int) access
, typeof(EAccess
));
175 if ((args
== null) || (args
.Length
== 0)) {
176 throw new ArgumentNullException(nameof(args
));
179 switch (args
.Length
) {
181 switch (args
[0].ToUpperInvariant()) {
183 return Task
.FromResult(ResponseRefreshManually(access
, bot
));
188 switch (args
[0].ToUpperInvariant()) {
190 return Task
.FromResult(ResponseRefreshManually(access
, Utilities
.GetArgsAsText(args
, 1, ","), steamID
));
196 return Task
.FromResult
<string?>(null);
199 public async Task
OnBotDestroy(Bot bot
) {
200 ArgumentNullException
.ThrowIfNull(bot
);
202 if (BotSubscriptions
.TryRemove(bot
, out IDisposable
? subscription
)) {
203 subscription
.Dispose();
206 if (BotSynchronizations
.TryRemove(bot
, out (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
) synchronization
)) {
207 // Ensure the semaphore is empty, otherwise we're risking disposed exceptions
208 await synchronization
.RefreshSemaphore
.WaitAsync().ConfigureAwait(false);
210 synchronization
.RefreshSemaphore
.Dispose();
212 await synchronization
.RefreshTimer
.DisposeAsync().ConfigureAwait(false);
216 public async Task
OnBotInit(Bot bot
) {
217 ArgumentNullException
.ThrowIfNull(bot
);
219 if (GlobalCache
== null) {
220 // We can't operate like this anyway, skip initialization of synchronization structures
224 SemaphoreSlim refreshSemaphore
= new(1, 1);
225 Timer refreshTimer
= new(OnBotRefreshTimer
, bot
, Timeout
.InfiniteTimeSpan
, Timeout
.InfiniteTimeSpan
);
227 if (!BotSynchronizations
.TryAdd(bot
, (refreshSemaphore
, refreshTimer
))) {
228 refreshSemaphore
.Dispose();
230 await refreshTimer
.DisposeAsync().ConfigureAwait(false);
234 public Task
OnBotSteamCallbacksInit(Bot bot
, CallbackManager callbackManager
) {
235 ArgumentNullException
.ThrowIfNull(bot
);
236 ArgumentNullException
.ThrowIfNull(callbackManager
);
238 if (BotSubscriptions
.TryRemove(bot
, out IDisposable
? subscription
)) {
239 subscription
.Dispose();
242 if (Config
is not { Enabled: true }
) {
243 return Task
.CompletedTask
;
246 subscription
= callbackManager
.Subscribe
<SteamApps
.LicenseListCallback
>(callback
=> OnLicenseList(bot
, callback
));
248 if (!BotSubscriptions
.TryAdd(bot
, subscription
)) {
249 subscription
.Dispose();
252 return Task
.CompletedTask
;
255 public Task
<IReadOnlyCollection
<ClientMsgHandler
>?> OnBotSteamHandlersInit(Bot bot
) => Task
.FromResult
<IReadOnlyCollection
<ClientMsgHandler
>?>(null);
257 public override Task
OnLoaded() {
258 Utilities
.WarnAboutIncompleteTranslation(Strings
.ResourceManager
);
260 return Task
.CompletedTask
;
263 public Task
OnPICSChanges(uint currentChangeNumber
, IReadOnlyDictionary
<uint, SteamApps
.PICSChangesCallback
.PICSChangeData
> appChanges
, IReadOnlyDictionary
<uint, SteamApps
.PICSChangesCallback
.PICSChangeData
> packageChanges
) {
264 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
265 ArgumentNullException
.ThrowIfNull(appChanges
);
266 ArgumentNullException
.ThrowIfNull(packageChanges
);
268 GlobalCache
?.OnPICSChanges(currentChangeNumber
, appChanges
);
270 return Task
.CompletedTask
;
273 public Task
OnPICSChangesRestart(uint currentChangeNumber
) {
274 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
276 GlobalCache
?.OnPICSChangesRestart(currentChangeNumber
);
278 return Task
.CompletedTask
;
281 private static async void OnBotRefreshTimer(object? state
) {
282 if (state
is not Bot bot
) {
283 throw new InvalidOperationException(nameof(state
));
286 await Refresh(bot
).ConfigureAwait(false);
289 private static async void OnLicenseList(Bot bot
, SteamApps
.LicenseListCallback callback
) {
290 ArgumentNullException
.ThrowIfNull(bot
);
291 ArgumentNullException
.ThrowIfNull(callback
);
293 if (Config
is not { Enabled: true }
) {
297 // Schedule a refresh in a while from now
298 if (!BotSynchronizations
.TryGetValue(bot
, out (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
) synchronization
)) {
302 if (!await synchronization
.RefreshSemaphore
.WaitAsync(0).ConfigureAwait(false)) {
303 // Another refresh is in progress, skip the refresh for now
308 synchronization
.RefreshTimer
.Change(TimeSpan
.FromMinutes(1), TimeSpan
.FromHours(SharedInfo
.MaximumHoursBetweenRefresh
));
310 synchronization
.RefreshSemaphore
.Release();
314 private static async void OnSubmissionTimer(object? state
= null) => await SubmitData().ConfigureAwait(false);
316 private static async Task
Refresh(Bot bot
) {
317 ArgumentNullException
.ThrowIfNull(bot
);
319 if (GlobalCache
== null) {
320 throw new InvalidOperationException(nameof(GlobalCache
));
323 if (ASF
.GlobalDatabase
== null) {
324 throw new InvalidOperationException(nameof(ASF
.GlobalDatabase
));
327 if (!BotSynchronizations
.TryGetValue(bot
, out (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
) synchronization
)) {
328 throw new InvalidOperationException(nameof(synchronization
));
331 if (!await synchronization
.RefreshSemaphore
.WaitAsync(0).ConfigureAwait(false)) {
335 SemaphoreSlim depotsRateLimitingSemaphore
= new(1, 1);
338 if (!bot
.IsConnectedAndLoggedOn
) {
342 HashSet
<uint> packageIDs
= bot
.OwnedPackages
.Where(static package
=> (Config
?.SecretPackageIDs
.Contains(package
.Key
) != true) && ((package
.Value
.PaymentMethod
!= EPaymentMethod
.AutoGrant
) || (Config
?.SkipAutoGrantPackages
== false))).Select(static package
=> package
.Key
).ToHashSet();
344 HashSet
<uint> appIDsToRefresh
= [];
346 foreach (uint packageID
in packageIDs
.Where(static packageID
=> Config
?.SecretPackageIDs
.Contains(packageID
) != true)) {
347 if (!ASF
.GlobalDatabase
.PackagesDataReadOnly
.TryGetValue(packageID
, out PackageData
? packageData
) || (packageData
.AppIDs
== null)) {
348 // ASF might not have the package info for us at the moment, we'll retry later
352 appIDsToRefresh
.UnionWith(packageData
.AppIDs
.Where(static appID
=> (Config
?.SecretAppIDs
.Contains(appID
) != true) && GlobalCache
.ShouldRefreshAppInfo(appID
)));
355 if (appIDsToRefresh
.Count
== 0) {
356 bot
.ArchiLogger
.LogGenericDebug(Strings
.BotNoAppsToRefresh
);
361 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotRetrievingTotalAppAccessTokens(appIDsToRefresh
.Count
));
363 HashSet
<uint> appIDsThisRound
= new(Math
.Min(appIDsToRefresh
.Count
, SharedInfo
.AppInfosPerSingleRequest
));
365 using (HashSet
<uint>.Enumerator enumerator
= appIDsToRefresh
.GetEnumerator()) {
367 if (!bot
.IsConnectedAndLoggedOn
) {
371 while ((appIDsThisRound
.Count
< SharedInfo
.AppInfosPerSingleRequest
) && enumerator
.MoveNext()) {
372 appIDsThisRound
.Add(enumerator
.Current
);
375 if (appIDsThisRound
.Count
== 0) {
379 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotRetrievingAppAccessTokens(appIDsThisRound
.Count
));
381 SteamApps
.PICSTokensCallback response
;
384 response
= await bot
.SteamApps
.PICSGetAccessTokens(appIDsThisRound
, []).ToLongRunningTask().ConfigureAwait(false);
385 } catch (Exception e
) {
386 bot
.ArchiLogger
.LogGenericWarningException(e
);
388 appIDsThisRound
.Clear();
393 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotFinishedRetrievingAppAccessTokens(appIDsThisRound
.Count
));
395 appIDsThisRound
.Clear();
397 GlobalCache
.UpdateAppTokens(response
.AppTokens
, response
.AppTokensDenied
);
401 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotFinishedRetrievingTotalAppAccessTokens(appIDsToRefresh
.Count
));
402 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotRetrievingTotalDepots(appIDsToRefresh
.Count
));
404 (_
, FrozenSet
<uint>? knownDepotIDs
) = await GlobalCache
.KnownDepotIDs
.GetValue(ECacheFallback
.SuccessPreviously
).ConfigureAwait(false);
406 using (HashSet
<uint>.Enumerator enumerator
= appIDsToRefresh
.GetEnumerator()) {
408 if (!bot
.IsConnectedAndLoggedOn
) {
412 while ((appIDsThisRound
.Count
< SharedInfo
.AppInfosPerSingleRequest
) && enumerator
.MoveNext()) {
413 appIDsThisRound
.Add(enumerator
.Current
);
416 if (appIDsThisRound
.Count
== 0) {
420 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotRetrievingAppInfos(appIDsThisRound
.Count
));
422 AsyncJobMultiple
<SteamApps
.PICSProductInfoCallback
>.ResultSet response
;
425 response
= await bot
.SteamApps
.PICSGetProductInfo(appIDsThisRound
.Select(static appID
=> new SteamApps
.PICSRequest(appID
, GlobalCache
.GetAppToken(appID
))), []).ToLongRunningTask().ConfigureAwait(false);
426 } catch (Exception e
) {
427 bot
.ArchiLogger
.LogGenericWarningException(e
);
429 appIDsThisRound
.Clear();
434 if (response
.Results
== null) {
435 bot
.ArchiLogger
.LogGenericWarning(ArchiSteamFarm
.Localization
.Strings
.FormatWarningFailedWithError(nameof(response
.Results
)));
437 appIDsThisRound
.Clear();
442 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotFinishedRetrievingAppInfos(appIDsThisRound
.Count
));
444 appIDsThisRound
.Clear();
446 Dictionary
<uint, uint> appChangeNumbers
= new();
448 uint depotKeysSuccessful
= 0;
449 uint depotKeysTotal
= 0;
451 foreach (SteamApps
.PICSProductInfoCallback
.PICSProductInfo app
in response
.Results
.SelectMany(static result
=> result
.Apps
.Values
)) {
452 appChangeNumbers
[app
.ID
] = app
.ChangeNumber
;
454 bool shouldFetchMainKey
= false;
456 foreach (KeyValue depot
in app
.KeyValues
["depots"].Children
) {
457 if (!uint.TryParse(depot
.Name
, out uint depotID
) || (knownDepotIDs
?.Contains(depotID
) == true) || (Config
?.SecretDepotIDs
.Contains(depotID
) == true) || !GlobalCache
.ShouldRefreshDepotKey(depotID
)) {
463 await depotsRateLimitingSemaphore
.WaitAsync().ConfigureAwait(false);
466 SteamApps
.DepotKeyCallback depotResponse
= await bot
.SteamApps
.GetDepotDecryptionKey(depotID
, app
.ID
).ToLongRunningTask().ConfigureAwait(false);
468 depotKeysSuccessful
++;
470 if (depotResponse
.Result
!= EResult
.OK
) {
474 shouldFetchMainKey
= true;
476 GlobalCache
.UpdateDepotKey(depotResponse
);
477 } catch (Exception e
) {
478 // We can still try other depots
479 bot
.ArchiLogger
.LogGenericWarningException(e
);
481 Utilities
.InBackground(
483 await Task
.Delay(DepotsRateLimitingDelay
).ConfigureAwait(false);
485 // ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
486 depotsRateLimitingSemaphore
.Release();
492 // Consider fetching main appID key only if we've actually considered some new depots for resolving
493 if (shouldFetchMainKey
&& (knownDepotIDs
?.Contains(app
.ID
) != true) && GlobalCache
.ShouldRefreshDepotKey(app
.ID
)) {
496 await depotsRateLimitingSemaphore
.WaitAsync().ConfigureAwait(false);
499 SteamApps
.DepotKeyCallback depotResponse
= await bot
.SteamApps
.GetDepotDecryptionKey(app
.ID
, app
.ID
).ToLongRunningTask().ConfigureAwait(false);
501 depotKeysSuccessful
++;
503 GlobalCache
.UpdateDepotKey(depotResponse
);
504 } catch (Exception e
) {
505 // We can still try other depots
506 bot
.ArchiLogger
.LogGenericWarningException(e
);
508 Utilities
.InBackground(
510 await Task
.Delay(DepotsRateLimitingDelay
).ConfigureAwait(false);
512 // ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
513 depotsRateLimitingSemaphore
.Release();
520 if (depotKeysTotal
> 0) {
521 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotFinishedRetrievingDepotKeys(depotKeysSuccessful
, depotKeysTotal
));
524 if (depotKeysSuccessful
< depotKeysTotal
) {
525 // We're not going to record app change numbers, as we didn't fetch all the depot keys we wanted
529 GlobalCache
.UpdateAppChangeNumbers(appChangeNumbers
);
533 bot
.ArchiLogger
.LogGenericInfo(Strings
.FormatBotFinishedRetrievingTotalDepots(appIDsToRefresh
.Count
));
535 if (Config
?.Enabled
== true) {
536 TimeSpan timeSpan
= TimeSpan
.FromHours(SharedInfo
.MaximumHoursBetweenRefresh
);
538 synchronization
.RefreshTimer
.Change(timeSpan
, timeSpan
);
541 await depotsRateLimitingSemaphore
.WaitAsync().ConfigureAwait(false);
543 synchronization
.RefreshSemaphore
.Release();
545 depotsRateLimitingSemaphore
.Dispose();
549 private static string? ResponseRefreshManually(EAccess access
, Bot bot
) {
550 if (!Enum
.IsDefined(access
)) {
551 throw new InvalidEnumArgumentException(nameof(access
), (int) access
, typeof(EAccess
));
554 ArgumentNullException
.ThrowIfNull(bot
);
556 if (access
< EAccess
.Master
) {
557 return access
> EAccess
.None
? bot
.Commands
.FormatBotResponse(ArchiSteamFarm
.Localization
.Strings
.ErrorAccessDenied
) : null;
560 if (GlobalCache
== null) {
561 return bot
.Commands
.FormatBotResponse(ArchiSteamFarm
.Localization
.Strings
.FormatWarningFailedWithError(nameof(GlobalCache
)));
564 Utilities
.InBackground(
566 await Refresh(bot
).ConfigureAwait(false);
567 await SubmitData().ConfigureAwait(false);
571 return bot
.Commands
.FormatBotResponse(ArchiSteamFarm
.Localization
.Strings
.Done
);
574 private static string? ResponseRefreshManually(EAccess access
, string botNames
, ulong steamID
= 0) {
575 if (!Enum
.IsDefined(access
)) {
576 throw new InvalidEnumArgumentException(nameof(access
), (int) access
, typeof(EAccess
));
579 ArgumentException
.ThrowIfNullOrEmpty(botNames
);
581 if ((steamID
!= 0) && !new SteamID(steamID
).IsIndividualAccount
) {
582 throw new ArgumentOutOfRangeException(nameof(steamID
));
585 HashSet
<Bot
>? bots
= Bot
.GetBots(botNames
);
587 if ((bots
== null) || (bots
.Count
== 0)) {
588 return access
>= EAccess
.Owner
? Commands
.FormatStaticResponse(ArchiSteamFarm
.Localization
.Strings
.FormatBotNotFound(botNames
)) : null;
591 if (bots
.RemoveWhere(bot
=> Commands
.GetProxyAccess(bot
, access
, steamID
) < EAccess
.Master
) > 0) {
592 if (bots
.Count
== 0) {
593 return access
>= EAccess
.Owner
? Commands
.FormatStaticResponse(ArchiSteamFarm
.Localization
.Strings
.FormatBotNotFound(botNames
)) : null;
597 if (GlobalCache
== null) {
598 return Commands
.FormatStaticResponse(ArchiSteamFarm
.Localization
.Strings
.FormatWarningFailedWithError(nameof(GlobalCache
)));
601 Utilities
.InBackground(
603 await Utilities
.InParallel(bots
.Select(static bot
=> Refresh(bot
))).ConfigureAwait(false);
605 await SubmitData().ConfigureAwait(false);
609 return Commands
.FormatStaticResponse(ArchiSteamFarm
.Localization
.Strings
.Done
);
612 private static async Task
SubmitData(CancellationToken cancellationToken
= default) {
613 if (Bot
.Bots
== null) {
614 throw new InvalidOperationException(nameof(Bot
.Bots
));
617 if (GlobalCache
== null) {
618 throw new InvalidOperationException(nameof(GlobalCache
));
621 if (ASF
.WebBrowser
== null) {
622 throw new InvalidOperationException(nameof(ASF
.WebBrowser
));
625 if (LastUploadAt
+ TimeSpan
.FromMinutes(SharedInfo
.MinimumMinutesBetweenUploads
) > DateTimeOffset
.UtcNow
) {
629 if (!await SubmissionSemaphore
.WaitAsync(0, cancellationToken
).ConfigureAwait(false)) {
634 Dictionary
<uint, ulong> appTokens
= GlobalCache
.GetAppTokensForSubmission();
635 Dictionary
<uint, ulong> packageTokens
= GlobalCache
.GetPackageTokensForSubmission();
636 Dictionary
<uint, string> depotKeys
= GlobalCache
.GetDepotKeysForSubmission();
638 if ((appTokens
.Count
== 0) && (packageTokens
.Count
== 0) && (depotKeys
.Count
== 0)) {
639 ASF
.ArchiLogger
.LogGenericInfo(Strings
.SubmissionNoNewData
);
644 ulong contributorSteamID
= ASF
.GlobalConfig
is { SteamOwnerID: > 0 }
&& new SteamID(ASF
.GlobalConfig
.SteamOwnerID
).IsIndividualAccount
? ASF
.GlobalConfig
.SteamOwnerID
: Bot
.Bots
.Values
.Where(static bot
=> bot
.SteamID
> 0).MaxBy(static bot
=> bot
.OwnedPackages
.Count
)?.SteamID
?? 0;
646 if (contributorSteamID
== 0) {
647 ASF
.ArchiLogger
.LogGenericError(Strings
.FormatSubmissionNoContributorSet(nameof(ASF
.GlobalConfig
.SteamOwnerID
)));
652 Uri request
= new($"{SharedInfo.ServerURL}/submit");
653 SubmitRequest data
= new(contributorSteamID
, appTokens
, packageTokens
, depotKeys
);
655 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionInProgress(appTokens
.Count
, packageTokens
.Count
, depotKeys
.Count
));
657 ObjectResponse
<SubmitResponse
>? response
= await ASF
.WebBrowser
.UrlPostToJsonObject
<SubmitResponse
, SubmitRequest
>(request
, data
: data
, requestOptions
: WebBrowser
.ERequestOptions
.ReturnClientErrors
| WebBrowser
.ERequestOptions
.AllowInvalidBodyOnErrors
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
659 if (response
== null) {
660 ASF
.ArchiLogger
.LogGenericWarning(ArchiSteamFarm
.Localization
.Strings
.WarningFailed
);
665 // We've communicated with the server and didn't timeout, regardless of the success, this was the last upload attempt
666 LastUploadAt
= DateTimeOffset
.UtcNow
;
668 if (response
.StatusCode
.IsClientErrorCode()) {
669 ASF
.ArchiLogger
.LogGenericWarning(ArchiSteamFarm
.Localization
.Strings
.FormatWarningFailedWithError(response
.StatusCode
));
671 switch (response
.StatusCode
) {
672 case HttpStatusCode
.Forbidden when Config
?.Enabled
== true:
673 // SteamDB told us to stop submitting data for now
674 // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
675 lock (SubmissionSemaphore
) {
676 SubmissionTimer
.Change(Timeout
.InfiniteTimeSpan
, Timeout
.InfiniteTimeSpan
);
680 case HttpStatusCode
.Conflict
:
681 // SteamDB told us to reset our cache
682 GlobalCache
.Reset(true);
685 case HttpStatusCode
.TooManyRequests when Config
?.Enabled
== true:
686 // SteamDB told us to try again later
687 #pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
688 TimeSpan startIn
= TimeSpan
.FromMinutes(Random
.Shared
.Next(SharedInfo
.MinimumMinutesBeforeFirstUpload
, SharedInfo
.MaximumMinutesBeforeFirstUpload
));
689 #pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
691 // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
692 lock (SubmissionSemaphore
) {
693 SubmissionTimer
.Change(startIn
, TimeSpan
.FromHours(SharedInfo
.HoursBetweenUploads
));
696 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionFailedTooManyRequests(startIn
.ToHumanReadable()));
704 if (response
.Content
is not { Success: true }
) {
705 ASF
.ArchiLogger
.LogGenericError(ArchiSteamFarm
.Localization
.Strings
.WarningFailed
);
710 if (response
.Content
.Data
== null) {
711 ASF
.ArchiLogger
.LogGenericError(ArchiSteamFarm
.Localization
.Strings
.FormatErrorIsInvalid(nameof(response
.Content
.Data
)));
716 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessful(response
.Content
.Data
.NewApps
.Count
, response
.Content
.Data
.VerifiedApps
.Count
, response
.Content
.Data
.NewPackages
.Count
, response
.Content
.Data
.VerifiedPackages
.Count
, response
.Content
.Data
.NewDepots
.Count
, response
.Content
.Data
.VerifiedDepots
.Count
));
718 GlobalCache
.UpdateSubmittedData(appTokens
, packageTokens
, depotKeys
);
720 if (!response
.Content
.Data
.NewApps
.IsEmpty
) {
721 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessfulNewApps(string.Join(", ", response
.Content
.Data
.NewApps
)));
724 if (!response
.Content
.Data
.VerifiedApps
.IsEmpty
) {
725 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessfulVerifiedApps(string.Join(", ", response
.Content
.Data
.VerifiedApps
)));
728 if (!response
.Content
.Data
.NewPackages
.IsEmpty
) {
729 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessfulNewPackages(string.Join(", ", response
.Content
.Data
.NewPackages
)));
732 if (!response
.Content
.Data
.VerifiedPackages
.IsEmpty
) {
733 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessfulVerifiedPackages(string.Join(", ", response
.Content
.Data
.VerifiedPackages
)));
736 if (!response
.Content
.Data
.NewDepots
.IsEmpty
) {
737 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessfulNewDepots(string.Join(", ", response
.Content
.Data
.NewDepots
)));
740 if (!response
.Content
.Data
.VerifiedDepots
.IsEmpty
) {
741 ASF
.ArchiLogger
.LogGenericInfo(Strings
.FormatSubmissionSuccessfulVerifiedDepots(string.Join(", ", response
.Content
.Data
.VerifiedDepots
)));
744 SubmissionSemaphore
.Release();