mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-16 06:20:34 +00:00
Initial SteamTokenDumper upload
This commit is contained in:
@@ -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>
|
||||||
294
ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalCache.cs
Normal file
294
ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalCache.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.Tests", "Arc
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.CustomPlugins.ExamplePlugin", "ArchiSteamFarm.CustomPlugins.ExamplePlugin\ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj", "{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.CustomPlugins.ExamplePlugin", "ArchiSteamFarm.CustomPlugins.ExamplePlugin\ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj", "{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{2C935C25-1B03-4C55-81C9-DF1D472D72F4}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {D7D54143-C857-4B76-A219-0E98C5BC4895}
|
|
||||||
RESX_ShowErrorsInErrorList = False
|
RESX_ShowErrorsInErrorList = False
|
||||||
|
SolutionGuid = {D7D54143-C857-4B76-A219-0E98C5BC4895}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -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.
|
/// 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>
|
/// </summary>
|
||||||
/// <returns>The most recent change number from which you're fine to receive <see cref="OnPICSChanges" /></returns>
|
/// <returns>The most recent change number from which you're fine to receive <see cref="OnPICSChanges" /></returns>
|
||||||
|
[NotNull]
|
||||||
Task<uint> GetPreferredChangeNumberToStartFrom();
|
Task<uint> GetPreferredChangeNumberToStartFrom();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ using ArchiSteamFarm.Plugins;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace ArchiSteamFarm {
|
namespace ArchiSteamFarm {
|
||||||
internal static class SharedInfo {
|
public static class SharedInfo {
|
||||||
|
[PublicAPI]
|
||||||
|
public const string ConfigDirectory = "config";
|
||||||
|
|
||||||
internal const ulong ArchiSteamID = 76561198006963719;
|
internal const ulong ArchiSteamID = 76561198006963719;
|
||||||
internal const string ArchivalLogFile = "log.{#}.txt";
|
internal const string ArchivalLogFile = "log.{#}.txt";
|
||||||
internal const string ArchivalLogsDirectory = "logs";
|
internal const string ArchivalLogsDirectory = "logs";
|
||||||
@@ -34,7 +37,6 @@ namespace ArchiSteamFarm {
|
|||||||
internal const ulong ASFGroupSteamID = 103582791440160998;
|
internal const ulong ASFGroupSteamID = 103582791440160998;
|
||||||
internal const string AssemblyDocumentation = AssemblyName + ".xml";
|
internal const string AssemblyDocumentation = AssemblyName + ".xml";
|
||||||
internal const string AssemblyName = nameof(ArchiSteamFarm);
|
internal const string AssemblyName = nameof(ArchiSteamFarm);
|
||||||
internal const string ConfigDirectory = "config";
|
|
||||||
internal const string DatabaseExtension = ".db";
|
internal const string DatabaseExtension = ".db";
|
||||||
internal const string DebugDirectory = "debug";
|
internal const string DebugDirectory = "debug";
|
||||||
internal const string EnvironmentVariableCryptKey = ASF + "_CRYPTKEY";
|
internal const string EnvironmentVariableCryptKey = ASF + "_CRYPTKEY";
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ environment:
|
|||||||
NET_CORE_VERSION: netcoreapp3.1
|
NET_CORE_VERSION: netcoreapp3.1
|
||||||
NET_FRAMEWORK_VERSION: net48
|
NET_FRAMEWORK_VERSION: net48
|
||||||
NODE_JS_VERSION: lts
|
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!
|
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:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
|
|||||||
Reference in New Issue
Block a user