Initial SteamTokenDumper upload

This commit is contained in:
JustArchi
2020-06-13 12:08:21 +02:00
parent 32b376d85a
commit 2a43a87e08
11 changed files with 970 additions and 4 deletions

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="4.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,294 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Helpers;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal sealed class GlobalCache : SerializableFile {
[NotNull]
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, nameof(SteamTokenDumper) + ".cache");
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, uint> AppChangeNumbers = new ConcurrentDictionary<uint, uint>();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> AppTokens = new ConcurrentDictionary<uint, ulong>();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> DepotKeys = new ConcurrentDictionary<uint, string>();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> PackageTokens = new ConcurrentDictionary<uint, ulong>();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentHashSet<uint> SubmittedAppIDs = new ConcurrentHashSet<uint>();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentHashSet<uint> SubmittedDepotIDs = new ConcurrentHashSet<uint>();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentHashSet<uint> SubmittedPackageIDs = new ConcurrentHashSet<uint>();
[JsonProperty(Required = Required.DisallowNull)]
internal uint LastChangeNumber { get; private set; }
internal GlobalCache() => FilePath = SharedFilePath;
internal ulong GetAppToken(uint appID) => AppTokens[appID];
[NotNull]
internal Dictionary<uint, ulong> GetAppTokensForSubmission() => AppTokens.Where(appToken => !SubmittedAppIDs.Contains(appToken.Key)).ToDictionary(appToken => appToken.Key, appToken => appToken.Value);
[NotNull]
internal Dictionary<uint, string> GetDepotKeysForSubmission() => DepotKeys.Where(depotKey => !SubmittedDepotIDs.Contains(depotKey.Key)).ToDictionary(depotKey => depotKey.Key, depotKey => depotKey.Value);
[NotNull]
internal Dictionary<uint, ulong> GetPackageTokensForSubmission() => PackageTokens.Where(packageToken => !SubmittedPackageIDs.Contains(packageToken.Key)).ToDictionary(packageToken => packageToken.Key, packageToken => packageToken.Value);
[ItemNotNull]
internal static async Task<GlobalCache> Load() {
if (!File.Exists(SharedFilePath)) {
return new GlobalCache();
}
GlobalCache globalCache = null;
try {
string json = await RuntimeCompatibility.File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
if (!string.IsNullOrEmpty(json)) {
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
if (globalCache == null) {
ASF.ArchiLogger.LogGenericError($"{nameof(GlobalCache)} could not be loaded, a fresh instance will be initialized.");
globalCache = new GlobalCache();
}
return globalCache;
}
internal async Task OnPICSChanges(uint currentChangeNumber, [NotNull] IReadOnlyCollection<KeyValuePair<uint, SteamApps.PICSChangesCallback.PICSChangeData>> appChanges) {
if ((currentChangeNumber == 0) || (appChanges == null)) {
throw new ArgumentNullException(nameof(appChanges));
}
if (currentChangeNumber <= LastChangeNumber) {
return;
}
ASF.ArchiLogger.LogGenericTrace($"{LastChangeNumber} => {currentChangeNumber}");
LastChangeNumber = currentChangeNumber;
foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) {
if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (appData.ChangeNumber <= previousChangeNumber)) {
continue;
}
AppChangeNumbers.TryRemove(appID, out _);
ASF.ArchiLogger.LogGenericTrace($"App needs refresh: {appID}");
}
await Save().ConfigureAwait(false);
}
internal async Task OnPICSChangesRestart(uint currentChangeNumber) {
if (currentChangeNumber == 0) {
throw new ArgumentNullException();
}
if (currentChangeNumber <= LastChangeNumber) {
return;
}
ASF.ArchiLogger.LogGenericDebug($"RESET {LastChangeNumber} => {currentChangeNumber}");
LastChangeNumber = currentChangeNumber;
AppChangeNumbers.Clear();
await Save().ConfigureAwait(false);
}
internal bool ShouldRefreshAppInfo(uint appID) => !AppChangeNumbers.ContainsKey(appID);
internal bool ShouldRefreshDepotKey(uint depotID) => !DepotKeys.ContainsKey(depotID);
internal async Task UpdateAppChangeNumbers([NotNull] IReadOnlyCollection<KeyValuePair<uint, uint>> appChangeNumbers) {
if (appChangeNumbers == null) {
throw new ArgumentNullException(nameof(appChangeNumbers));
}
bool save = false;
foreach ((uint appID, uint changeNumber) in appChangeNumbers) {
if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber == changeNumber)) {
continue;
}
AppChangeNumbers[appID] = changeNumber;
save = true;
}
if (save) {
await Save().ConfigureAwait(false);
}
}
internal async Task UpdateAppTokens([NotNull] IReadOnlyCollection<KeyValuePair<uint, ulong>> appTokens, [NotNull] IReadOnlyCollection<uint> publicAppIDs) {
if ((appTokens == null) || (publicAppIDs == null)) {
throw new ArgumentNullException(nameof(appTokens) + " || " + nameof(publicAppIDs));
}
bool save = false;
foreach ((uint appID, ulong appToken) in appTokens) {
if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == appToken)) {
continue;
}
AppTokens[appID] = appToken;
if (appToken == 0) {
// Backend is not interested in zero access tokens
SubmittedAppIDs.Add(appID);
}
save = true;
}
foreach (uint appID in publicAppIDs) {
if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == 0)) {
continue;
}
AppTokens[appID] = 0;
// Backend is not interested in zero access tokens
SubmittedAppIDs.Add(appID);
save = true;
}
if (save) {
await Save().ConfigureAwait(false);
}
}
internal async Task UpdateDepotKeys([NotNull] IReadOnlyCollection<SteamApps.DepotKeyCallback> depotKeyResults) {
if (depotKeyResults == null) {
throw new ArgumentNullException(nameof(depotKeyResults));
}
bool save = false;
foreach (SteamApps.DepotKeyCallback depotKeyResult in depotKeyResults) {
if ((depotKeyResult == null) || (depotKeyResult.Result != EResult.OK)) {
continue;
}
string depotKey = BitConverter.ToString(depotKeyResult.DepotKey).Replace("-", "");
if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string previousDepotKey) && (previousDepotKey == depotKey)) {
continue;
}
DepotKeys[depotKeyResult.DepotID] = depotKey;
if (string.IsNullOrEmpty(depotKey)) {
// Backend is not interested in zero depot keys
SubmittedDepotIDs.Add(depotKeyResult.DepotID);
}
save = true;
}
if (save) {
await Save().ConfigureAwait(false);
}
}
internal async Task UpdatePackageTokens([NotNull] IReadOnlyCollection<KeyValuePair<uint, ulong>> packageTokens) {
if (packageTokens == null) {
throw new ArgumentNullException(nameof(packageTokens));
}
bool save = false;
foreach ((uint packageID, ulong packageToken) in packageTokens) {
if (PackageTokens.TryGetValue(packageID, out ulong previousPackageToken) && (previousPackageToken == packageToken)) {
continue;
}
PackageTokens[packageID] = packageToken;
if (packageToken == 0) {
// Backend is not interested in zero access tokens
SubmittedPackageIDs.Add(packageID);
}
save = true;
}
if (save) {
await Save().ConfigureAwait(false);
}
}
internal async Task UpdateSubmittedData([NotNull] IReadOnlyCollection<uint> appIDs, [NotNull] IReadOnlyCollection<uint> packageIDs, [NotNull] IReadOnlyCollection<uint> depotIDs) {
if ((appIDs == null) || (packageIDs == null) || (depotIDs == null)) {
throw new ArgumentNullException(nameof(appIDs) + " || " + nameof(packageIDs) + " || " + nameof(depotIDs));
}
bool save = false;
foreach (uint _ in appIDs.Where(appID => SubmittedAppIDs.Add(appID))) {
save = true;
}
foreach (uint _ in packageIDs.Where(packageID => SubmittedPackageIDs.Add(packageID))) {
save = true;
}
foreach (uint _ in depotIDs.Where(depotID => SubmittedDepotIDs.Add(depotID))) {
save = true;
}
if (save) {
await Save().ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,81 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal sealed class RequestData {
#pragma warning disable IDE0052
[JsonProperty(PropertyName = "apps", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Apps;
#pragma warning restore IDE0052
#pragma warning disable IDE0052
[JsonProperty(PropertyName = "depots", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Depots;
#pragma warning restore IDE0052
#pragma warning disable IDE0052
[JsonProperty(PropertyName = "guid", Required = Required.Always)]
private readonly string Guid = ASF.GlobalDatabase.Guid.ToString("N");
#pragma warning restore IDE0052
private readonly ulong SteamID;
#pragma warning disable IDE0052
[JsonProperty(PropertyName = "subs", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Subs;
#pragma warning restore IDE0052
#pragma warning disable IDE0051, 414
[JsonProperty(PropertyName = "token", Required = Required.Always)]
private readonly string Token = SharedInfo.Token;
#pragma warning restore IDE0051, 414
#pragma warning disable IDE0051, 414
[JsonProperty(PropertyName = "v", Required = Required.Always)]
private readonly byte Version = SharedInfo.ApiVersion;
#pragma warning restore IDE0051, 414
#pragma warning disable IDE0051
[JsonProperty(PropertyName = "steamid", Required = Required.Always)]
[NotNull]
private string SteamIDText => new SteamID(SteamID).Render();
#pragma warning restore IDE0051
internal RequestData(ulong steamID, [NotNull] IEnumerable<KeyValuePair<uint, ulong>> apps, [NotNull] IEnumerable<KeyValuePair<uint, ulong>> accessTokens, [NotNull] IEnumerable<KeyValuePair<uint, string>> depots) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount || (apps == null) || (accessTokens == null) || (depots == null)) {
throw new ArgumentNullException(nameof(steamID) + " || " + nameof(apps) + " || " + nameof(accessTokens) + " || " + nameof(depots));
}
SteamID = steamID;
Apps = apps.ToImmutableDictionary(app => app.Key.ToString(), app => app.Value.ToString());
Subs = accessTokens.ToImmutableDictionary(package => package.Key.ToString(), package => package.Value.ToString());
Depots = depots.ToImmutableDictionary(depot => depot.Key.ToString(), depot => depot.Value);
}
}
}

View File

@@ -0,0 +1,61 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ResponseData {
#pragma warning disable 649
[JsonProperty(PropertyName = "data", Required = Required.Always)]
internal readonly InternalData Data;
#pragma warning restore 649
#pragma warning disable 649
[JsonProperty(PropertyName = "success", Required = Required.Always)]
internal readonly bool Success;
#pragma warning restore 649
[JsonConstructor]
private ResponseData() { }
internal sealed class InternalData {
#pragma warning disable 649
[JsonProperty(PropertyName = "new_apps", Required = Required.Always)]
internal readonly uint NewAppsCount;
#pragma warning restore 649
#pragma warning disable 649
[JsonProperty(PropertyName = "new_depots", Required = Required.Always)]
internal readonly uint NewDepotsCount;
#pragma warning restore 649
#pragma warning disable 649
[JsonProperty(PropertyName = "new_subs", Required = Required.Always)]
internal readonly uint NewSubsCount;
#pragma warning restore 649
[JsonConstructor]
private InternalData() { }
}
}
}

View File

@@ -0,0 +1,34 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal static class SharedInfo {
internal const byte ApiVersion = 1;
internal const ushort ItemsPerSingleRequest = 2048; // Should be synchronized with TimeoutForLongRunningTasksInSeconds
internal const byte MaximumHoursBetweenRefresh = 8; // Per single bot account, makes sense to be 2 or 3 times less than MinimumHoursBetweenUploads
internal const byte MaximumMinutesBeforeFirstUpload = 60; // Must be greater or equal to MinimumMinutesBeforeFirstUpload
internal const byte MinimumHoursBetweenUploads = 24;
internal const byte MinimumMinutesBeforeFirstUpload = 10; // Must be less or equal to MaximumMinutesBeforeFirstUpload
internal const string ServerURL = "https://asf-token-dumper.xpaw.me";
internal const byte TimeoutForLongRunningTasksInSeconds = 60; // Should be synchronized with ItemsPerSingleRequest
internal const string Token = "STEAM_TOKEN_DUMPER_TOKEN";
}
}

View File

@@ -0,0 +1,40 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
internal static class StaticHelpers {
[NotNull]
internal static Task<T> ToLongRunningTask<T>([NotNull] this AsyncJob<T> job) where T : CallbackMsg {
if (job == null) {
throw new ArgumentNullException(nameof(job));
}
job.Timeout = TimeSpan.FromSeconds(SharedInfo.TimeoutForLongRunningTasksInSeconds);
return job.ToTask();
}
}
}

View File

@@ -0,0 +1,430 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Plugins;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper {
[Export(typeof(IPlugin))]
[UsedImplicitly]
internal sealed class SteamTokenDumperPlugin : IASF, IBot, IBotSteamClient, ISteamPICSChanges {
private static readonly ConcurrentDictionary<Bot, IDisposable> BotSubscriptions = new ConcurrentDictionary<Bot, IDisposable>();
private static readonly ConcurrentDictionary<Bot, (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer)> BotSynchronizations = new ConcurrentDictionary<Bot, (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer)>();
private static readonly SemaphoreSlim SubmissionSemaphore = new SemaphoreSlim(1, 1);
private static readonly Timer SubmissionTimer = new Timer(async e => await SubmitData().ConfigureAwait(false));
private static GlobalCache GlobalCache;
private static bool IsEnabled;
public string Name => nameof(SteamTokenDumperPlugin);
public Version Version => typeof(SteamTokenDumperPlugin).Assembly.GetName().Version ?? throw new ArgumentNullException(nameof(Version));
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(IsEnabled ? GlobalCache?.LastChangeNumber ?? 0 : 0);
public void OnASFInit(IReadOnlyDictionary<string, JToken> additionalConfigProperties = null) {
const string enabledProperty = nameof(SteamTokenDumperPlugin) + "Enabled";
bool enabled = false;
if (additionalConfigProperties != null) {
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
try {
if (configProperty == enabledProperty) {
enabled = configValue.Value<bool>();
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
break;
}
}
}
IsEnabled = enabled;
if (!enabled) {
ASF.ArchiLogger.LogGenericInfo($"{Name} is currently disabled. If you'd like to help SteamDB in data submission, check out our wiki.");
return;
}
GlobalCache ??= GlobalCache.Load().Result;
TimeSpan startIn = TimeSpan.FromMinutes(Utilities.RandomNext(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
lock (SubmissionTimer) {
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.MinimumHoursBetweenUploads));
}
ASF.ArchiLogger.LogGenericInfo($"{Name} has been initialized successfully, thank you for your help. The first submission will happen in approximately {startIn.ToHumanReadable()} from now.");
}
public async void OnBotDestroy(Bot bot) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
if (BotSubscriptions.TryRemove(bot, out IDisposable subscription)) {
subscription.Dispose();
}
if (BotSynchronizations.TryRemove(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
synchronization.RefreshSemaphore.Dispose();
await synchronization.RefreshTimer.DisposeAsync().ConfigureAwait(false);
}
}
public async void OnBotInit(Bot bot) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
if (!IsEnabled) {
return;
}
SemaphoreSlim refreshSemaphore = new SemaphoreSlim(1, 1);
Timer refreshTimer = new Timer(async e => await Refresh(bot).ConfigureAwait(false));
if (!BotSynchronizations.TryAdd(bot, (refreshSemaphore, refreshTimer))) {
refreshSemaphore.Dispose();
await refreshTimer.DisposeAsync().ConfigureAwait(false);
}
}
public void OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
if ((bot == null) || (callbackManager == null)) {
throw new ArgumentNullException(nameof(bot) + " || " + nameof(callbackManager));
}
if (BotSubscriptions.TryRemove(bot, out IDisposable subscription)) {
subscription.Dispose();
}
if (!IsEnabled) {
return;
}
subscription = callbackManager.Subscribe<SteamApps.LicenseListCallback>(callback => OnLicenseList(bot, callback));
if (!BotSubscriptions.TryAdd(bot, subscription)) {
subscription.Dispose();
}
}
public IReadOnlyCollection<ClientMsgHandler> OnBotSteamHandlersInit(Bot bot) => null;
public void OnLoaded() { }
public async void OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
if ((currentChangeNumber == 0) || (appChanges == null) || (packageChanges == null)) {
throw new ArgumentNullException(nameof(currentChangeNumber) + " || " + nameof(appChanges) + " || " + nameof(packageChanges));
}
if (!IsEnabled) {
return;
}
if (GlobalCache == null) {
throw new ArgumentNullException(nameof(GlobalCache));
}
await GlobalCache.OnPICSChanges(currentChangeNumber, appChanges).ConfigureAwait(false);
}
public async void OnPICSChangesRestart(uint currentChangeNumber) {
if (currentChangeNumber == 0) {
throw new ArgumentNullException(nameof(currentChangeNumber));
}
if (!IsEnabled) {
return;
}
if (GlobalCache == null) {
throw new ArgumentNullException(nameof(GlobalCache));
}
await GlobalCache.OnPICSChangesRestart(currentChangeNumber).ConfigureAwait(false);
}
private static async void OnLicenseList([NotNull] Bot bot, [NotNull] SteamApps.LicenseListCallback callback) {
if ((bot == null) || (callback == null)) {
throw new ArgumentNullException(nameof(callback));
}
if (!IsEnabled) {
return;
}
if (GlobalCache == null) {
throw new ArgumentNullException(nameof(GlobalCache));
}
Dictionary<uint, ulong> packageTokens = callback.LicenseList.ToDictionary(license => license.PackageID, license => license.AccessToken);
await GlobalCache.UpdatePackageTokens(packageTokens).ConfigureAwait(false);
await Refresh(bot, packageTokens.Keys).ConfigureAwait(false);
}
private static async Task Refresh([NotNull] Bot bot, IReadOnlyCollection<uint> packageIDs = null) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
if (!IsEnabled) {
return;
}
if (GlobalCache == null) {
throw new ArgumentNullException(nameof(GlobalCache));
}
if (!BotSynchronizations.TryGetValue(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
throw new ArgumentNullException(nameof(synchronization));
}
if (!await synchronization.RefreshSemaphore.WaitAsync(0).ConfigureAwait(false)) {
return;
}
try {
if (!bot.IsConnectedAndLoggedOn) {
return;
}
packageIDs ??= bot.OwnedPackageIDsReadOnly;
HashSet<uint> appIDsToRefresh = new HashSet<uint>();
foreach (uint packageID in packageIDs) {
if (!ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out (uint ChangeNumber, HashSet<uint> AppIDs) packageData)) {
// ASF might not have the package info for us at the moment, we'll retry later
continue;
}
appIDsToRefresh.UnionWith(packageData.AppIDs.Where(appID => GlobalCache.ShouldRefreshAppInfo(appID)));
}
if (appIDsToRefresh.Count == 0) {
bot.ArchiLogger.LogGenericDebug($"There are no apps to refresh for {bot.BotName}.");
return;
}
bot.ArchiLogger.LogGenericInfo($"Retrieving a total of {appIDsToRefresh.Count} app access tokens...");
HashSet<uint> appIDsThisRound = new HashSet<uint>(Math.Min(appIDsToRefresh.Count, SharedInfo.ItemsPerSingleRequest));
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
while ((appIDsThisRound.Count < SharedInfo.ItemsPerSingleRequest) && enumerator.MoveNext()) {
appIDsThisRound.Add(enumerator.Current);
}
if (appIDsThisRound.Count == 0) {
break;
}
bot.ArchiLogger.LogGenericInfo($"Retrieving {appIDsThisRound.Count} app access tokens...");
SteamApps.PICSTokensCallback response;
try {
response = await bot.SteamApps.PICSGetAccessTokens(appIDsThisRound, Enumerable.Empty<uint>());
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return;
}
bot.ArchiLogger.LogGenericInfo($"Finished retrieving {appIDsThisRound.Count} app access tokens.");
appIDsThisRound.Clear();
await GlobalCache.UpdateAppTokens(response.AppTokens, response.AppTokensDenied).ConfigureAwait(false);
}
}
bot.ArchiLogger.LogGenericInfo($"Finished retrieving a total of {appIDsToRefresh.Count} app access tokens.");
bot.ArchiLogger.LogGenericInfo($"Retrieving all depots for a total of {appIDsToRefresh.Count} apps...");
appIDsThisRound.Clear();
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
while ((appIDsThisRound.Count < SharedInfo.ItemsPerSingleRequest) && enumerator.MoveNext()) {
appIDsThisRound.Add(enumerator.Current);
}
if (appIDsThisRound.Count == 0) {
break;
}
bot.ArchiLogger.LogGenericInfo($"Retrieving {appIDsThisRound.Count} app infos...");
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet response;
try {
response = await bot.SteamApps.PICSGetProductInfo(appIDsThisRound.Select(appID => new SteamApps.PICSRequest { ID = appID, AccessToken = GlobalCache.GetAppToken(appID), Public = false }), Enumerable.Empty<SteamApps.PICSRequest>());
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return;
}
if (response.Results == null) {
bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningFailedWithError, nameof(response.Results)));
return;
}
bot.ArchiLogger.LogGenericInfo($"Finished retrieving {appIDsThisRound.Count} app infos.");
appIDsThisRound.Clear();
Dictionary<uint, uint> appChangeNumbers = new Dictionary<uint, uint>();
HashSet<Task<SteamApps.DepotKeyCallback>> depotTasks = new HashSet<Task<SteamApps.DepotKeyCallback>>();
foreach (SteamApps.PICSProductInfoCallback.PICSProductInfo app in response.Results.SelectMany(result => result.Apps.Values)) {
appChangeNumbers[app.ID] = app.ChangeNumber;
if (GlobalCache.ShouldRefreshDepotKey(app.ID)) {
depotTasks.Add(bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask());
}
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
if (uint.TryParse(depot.Name, out uint depotID) && GlobalCache.ShouldRefreshDepotKey(depotID)) {
depotTasks.Add(bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask());
}
}
}
await GlobalCache.UpdateAppChangeNumbers(appChangeNumbers).ConfigureAwait(false);
if (depotTasks.Count > 0) {
bot.ArchiLogger.LogGenericInfo($"Retrieving {depotTasks.Count} depot keys...");
SteamApps.DepotKeyCallback[] results = await Task.WhenAll(depotTasks).ConfigureAwait(false);
bot.ArchiLogger.LogGenericInfo($"Finished retrieving {depotTasks.Count} depot keys.");
await GlobalCache.UpdateDepotKeys(results).ConfigureAwait(false);
}
}
}
bot.ArchiLogger.LogGenericInfo($"Finished retrieving all depot keys for a total of {appIDsToRefresh.Count} apps.");
} finally {
TimeSpan timeSpan = TimeSpan.FromHours(SharedInfo.MaximumHoursBetweenRefresh);
synchronization.RefreshTimer.Change(timeSpan, timeSpan);
synchronization.RefreshSemaphore.Release();
Utilities.InBackground(SubmitData);
}
}
private static async Task SubmitData() {
const string request = SharedInfo.ServerURL + "/submit";
if (!IsEnabled) {
return;
}
if (GlobalCache == null) {
throw new ArgumentNullException(nameof(GlobalCache));
}
if (!await SubmissionSemaphore.WaitAsync(0).ConfigureAwait(false)) {
ASF.ArchiLogger.LogGenericDebug($"Skipped {nameof(SubmitData)} trigger because there is already one in progress.");
return;
}
try {
Dictionary<uint, ulong> appTokens = GlobalCache.GetAppTokensForSubmission();
Dictionary<uint, ulong> packageTokens = GlobalCache.GetPackageTokensForSubmission();
Dictionary<uint, string> depotKeys = GlobalCache.GetDepotKeysForSubmission();
if ((appTokens.Count == 0) && (packageTokens.Count == 0) && (depotKeys.Count == 0)) {
ASF.ArchiLogger.LogGenericInfo("There is no new data to submit, everything up-to-date.");
return;
}
ulong contributorSteamID = (ASF.GlobalConfig.SteamOwnerID > 0) && new SteamID(ASF.GlobalConfig.SteamOwnerID).IsIndividualAccount ? ASF.GlobalConfig.SteamOwnerID : Bot.BotsReadOnly.Values.Where(bot => bot.SteamID > 0).OrderByDescending(bot => bot.OwnedPackageIDsReadOnly.Count).FirstOrDefault()?.SteamID ?? 0;
if (contributorSteamID == 0) {
ASF.ArchiLogger.LogGenericError($"Skipped {nameof(SubmitData)} trigger because there is no valid steamID we could classify as a contributor. Consider setting up {nameof(ASF.GlobalConfig.SteamOwnerID)} property.");
return;
}
RequestData requestData = new RequestData(contributorSteamID, appTokens, packageTokens, depotKeys);
ASF.ArchiLogger.LogGenericInfo($"Submitting registered apps/subs/depots: {appTokens.Count}/{packageTokens.Count}/{depotKeys.Count}...");
WebBrowser.ObjectResponse<ResponseData> response = await ASF.WebBrowser.UrlPostToJsonObject<ResponseData, RequestData>(request, requestData, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
if ((response?.Content == null) || response.StatusCode.IsClientErrorCode()) {
ASF.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
#if NETFRAMEWORK
if (response?.StatusCode == (HttpStatusCode) 429) {
#else
if (response?.StatusCode == HttpStatusCode.TooManyRequests) {
#endif
TimeSpan startIn = TimeSpan.FromMinutes(Utilities.RandomNext(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
lock (SubmissionTimer) {
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.MinimumHoursBetweenUploads));
}
ASF.ArchiLogger.LogGenericInfo($"The submission will happen in approximately {startIn.ToHumanReadable()} from now.");
}
return;
}
ASF.ArchiLogger.LogGenericInfo($"Data successfully submitted. Newly registered apps/subs/depots: {response.Content.Data.NewAppsCount}/{response.Content.Data.NewSubsCount}/{response.Content.Data.NewDepotsCount}.");
await GlobalCache.UpdateSubmittedData(appTokens.Keys, packageTokens.Keys, depotKeys.Keys).ConfigureAwait(false);
} finally {
SubmissionSemaphore.Release();
}
}
}
}

View File

@@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.Tests", "Arc
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.CustomPlugins.ExamplePlugin", "ArchiSteamFarm.CustomPlugins.ExamplePlugin\ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj", "{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.CustomPlugins.PeriodicGC", "ArchiSteamFarm.CustomPlugins.PeriodicGC\ArchiSteamFarm.CustomPlugins.PeriodicGC.csproj", "{2C935C25-1B03-4C55-81C9-DF1D472D72F4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.CustomPlugins.PeriodicGC", "ArchiSteamFarm.CustomPlugins.PeriodicGC\ArchiSteamFarm.CustomPlugins.PeriodicGC.csproj", "{2C935C25-1B03-4C55-81C9-DF1D472D72F4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper", "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.csproj", "{A9299EE5-AF67-4FCB-8D03-263B41819504}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -33,12 +35,16 @@ Global
{2C935C25-1B03-4C55-81C9-DF1D472D72F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C935C25-1B03-4C55-81C9-DF1D472D72F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C935C25-1B03-4C55-81C9-DF1D472D72F4}.Release|Any CPU.Build.0 = Release|Any CPU
{A9299EE5-AF67-4FCB-8D03-263B41819504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9299EE5-AF67-4FCB-8D03-263B41819504}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9299EE5-AF67-4FCB-8D03-263B41819504}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9299EE5-AF67-4FCB-8D03-263B41819504}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D7D54143-C857-4B76-A219-0E98C5BC4895}
RESX_ShowErrorsInErrorList = False
SolutionGuid = {D7D54143-C857-4B76-A219-0E98C5BC4895}
EndGlobalSection
EndGlobal

View File

@@ -31,6 +31,7 @@ namespace ArchiSteamFarm.Plugins {
/// ASF uses this method for determining the point in time from which it should keep history going upon a restart. The actual point in time that will be used is calculated as the lowest change number from all loaded plugins, to guarantee that no plugin will miss any changes, while allowing possible duplicates for those plugins that were already synchronized with newer changes. If you don't care about persistent state and just want to receive the ongoing history, you should return 0 (which is equal to "I'm fine with any"). If there won't be any plugin asking for a specific point in time, ASF will start returning entries since the start of the program.
/// </summary>
/// <returns>The most recent change number from which you're fine to receive <see cref="OnPICSChanges" /></returns>
[NotNull]
Task<uint> GetPreferredChangeNumberToStartFrom();
/// <summary>

View File

@@ -26,7 +26,10 @@ using ArchiSteamFarm.Plugins;
using JetBrains.Annotations;
namespace ArchiSteamFarm {
internal static class SharedInfo {
public static class SharedInfo {
[PublicAPI]
public const string ConfigDirectory = "config";
internal const ulong ArchiSteamID = 76561198006963719;
internal const string ArchivalLogFile = "log.{#}.txt";
internal const string ArchivalLogsDirectory = "logs";
@@ -34,7 +37,6 @@ namespace ArchiSteamFarm {
internal const ulong ASFGroupSteamID = 103582791440160998;
internal const string AssemblyDocumentation = AssemblyName + ".xml";
internal const string AssemblyName = nameof(ArchiSteamFarm);
internal const string ConfigDirectory = "config";
internal const string DatabaseExtension = ".db";
internal const string DebugDirectory = "debug";
internal const string EnvironmentVariableCryptKey = ASF + "_CRYPTKEY";

View File

@@ -16,6 +16,8 @@ environment:
NET_CORE_VERSION: netcoreapp3.1
NET_FRAMEWORK_VERSION: net48
NODE_JS_VERSION: lts
STEAM_TOKEN_DUMPER_TOKEN:
secure: uttQUE9ZK7BIa9SIbDkpUTMx7Slnl3zAPkRNzE465YgwxLdLEwv6yYR5QXCSZolb5Qq23Z/LmZNGd3M6B0+hbx3waWOeW2AiWvfCcnUmuT+3wfLJsgLbf1g4agFS7zsDgeRPfnNMzOxD8etelnA5YOOUMNB3RLw3fIdznNd+Fs6R0Ou3/1UavDuHKkbh1+A5
VARIANTS: generic generic-netf linux-arm linux-arm64 linux-x64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
matrix:
allow_failures: