2020-06-13 10:08:21 +00:00
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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]
2020-06-13 13:09:12 +00:00
internal sealed class SteamTokenDumperPlugin : OfficialPlugin , IASF , IBot , IBotSteamClient , ISteamPICSChanges {
2020-06-13 10:08:21 +00:00
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 ;
2020-06-13 13:09:12 +00:00
public override string Name = > nameof ( SteamTokenDumperPlugin ) ;
2020-06-13 10:08:21 +00:00
2020-06-13 13:09:12 +00:00
public override Version Version = > typeof ( SteamTokenDumperPlugin ) . Assembly . GetName ( ) . Version ? ? throw new ArgumentNullException ( nameof ( Version ) ) ;
2020-06-13 10:08:21 +00:00
public Task < uint > GetPreferredChangeNumberToStartFrom ( ) = > Task . FromResult ( IsEnabled ? GlobalCache ? . LastChangeNumber ? ? 0 : 0 ) ;
public void OnASFInit ( IReadOnlyDictionary < string , JToken > additionalConfigProperties = null ) {
2020-06-13 13:35:56 +00:00
if ( ! SharedInfo . HasValidToken ) {
ASF . ArchiLogger . LogGenericError ( $"{Name} has been disabled due to missing build token." ) ;
return ;
}
2020-06-13 10:08:21 +00:00
bool enabled = false ;
if ( additionalConfigProperties ! = null ) {
foreach ( ( string configProperty , JToken configValue ) in additionalConfigProperties ) {
try {
2020-06-13 13:35:56 +00:00
if ( configProperty = = SharedInfo . ConfigurationPropertyEnabled ) {
2020-06-13 10:08:21 +00:00
enabled = configValue . Value < bool > ( ) ;
}
} catch ( Exception e ) {
ASF . ArchiLogger . LogGenericException ( e ) ;
break ;
}
}
}
IsEnabled = enabled ;
if ( ! enabled ) {
2020-06-13 16:47:09 +00:00
ASF . ArchiLogger . LogGenericInfo ( $"{Name} is currently disabled. If you'd like to help SteamDB in data submission, check out our wiki for {nameof(SteamTokenDumperPlugin)}." ) ;
2020-06-13 10:08:21 +00:00
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 ;
2020-06-13 13:09:12 +00:00
public override void OnLoaded ( ) { }
2020-06-13 10:08:21 +00:00
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 ) ) ;
}
2020-06-13 19:45:37 +00:00
Dictionary < uint , ulong > packageTokens = callback . LicenseList . GroupBy ( license = > license . PackageID ) . ToDictionary ( group = > group . Key , group = > group . OrderByDescending ( license = > license . TimeCreated ) . First ( ) . AccessToken ) ;
2020-06-13 10:08:21 +00:00
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 ) {
2020-06-13 10:49:12 +00:00
if ( ! ASF . GlobalDatabase . PackagesDataReadOnly . TryGetValue ( packageID , out ( uint ChangeNumber , HashSet < uint > AppIDs ) packageData ) | | ( packageData . AppIDs = = null ) ) {
2020-06-13 10:08:21 +00:00
// 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..." ) ;
2020-06-15 14:25:28 +00:00
HashSet < uint > appIDsThisRound = new HashSet < uint > ( Math . Min ( appIDsToRefresh . Count , SharedInfo . AppInfosPerSingleRequest ) ) ;
2020-06-13 10:08:21 +00:00
using ( HashSet < uint > . Enumerator enumerator = appIDsToRefresh . GetEnumerator ( ) ) {
while ( true ) {
2020-06-15 14:25:28 +00:00
while ( ( appIDsThisRound . Count < SharedInfo . AppInfosPerSingleRequest ) & & enumerator . MoveNext ( ) ) {
2020-06-13 10:08:21 +00:00
appIDsThisRound . Add ( enumerator . Current ) ;
}
if ( appIDsThisRound . Count = = 0 ) {
break ;
}
2020-06-20 15:12:18 +00:00
if ( ! bot . IsConnectedAndLoggedOn ) {
return ;
}
2020-06-13 10:08:21 +00:00
bot . ArchiLogger . LogGenericInfo ( $"Retrieving {appIDsThisRound.Count} app access tokens..." ) ;
SteamApps . PICSTokensCallback response ;
try {
2020-06-15 14:55:57 +00:00
response = await bot . SteamApps . PICSGetAccessTokens ( appIDsThisRound , Enumerable . Empty < uint > ( ) ) . ToLongRunningTask ( ) . ConfigureAwait ( false ) ;
2020-06-13 10:08:21 +00:00
} catch ( Exception e ) {
bot . ArchiLogger . LogGenericWarningException ( e ) ;
return ;
}
2020-06-20 15:12:18 +00:00
if ( response = = null ) {
bot . ArchiLogger . LogGenericWarning ( string . Format ( Strings . WarningFailedWithError , nameof ( response ) ) ) ;
return ;
}
2020-06-13 10:08:21 +00:00
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 ) {
2020-06-15 14:25:28 +00:00
while ( ( appIDsThisRound . Count < SharedInfo . AppInfosPerSingleRequest ) & & enumerator . MoveNext ( ) ) {
2020-06-13 10:08:21 +00:00
appIDsThisRound . Add ( enumerator . Current ) ;
}
if ( appIDsThisRound . Count = = 0 ) {
break ;
}
2020-06-20 15:12:18 +00:00
if ( ! bot . IsConnectedAndLoggedOn ) {
return ;
}
2020-06-13 10:08:21 +00:00
bot . ArchiLogger . LogGenericInfo ( $"Retrieving {appIDsThisRound.Count} app infos..." ) ;
AsyncJobMultiple < SteamApps . PICSProductInfoCallback > . ResultSet response ;
try {
2020-06-15 14:55:57 +00:00
response = await bot . SteamApps . PICSGetProductInfo ( appIDsThisRound . Select ( appID = > new SteamApps . PICSRequest { ID = appID , AccessToken = GlobalCache . GetAppToken ( appID ) , Public = false } ) , Enumerable . Empty < SteamApps . PICSRequest > ( ) ) . ToLongRunningTask ( ) . ConfigureAwait ( false ) ;
2020-06-13 10:08:21 +00:00
} 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 > ( ) ;
2020-06-15 14:25:28 +00:00
2020-06-13 10:08:21 +00:00
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 ( ) ;
}
}
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 ( ) ;
}
}
}
}