Files
Torch/Torch/Utils/SteamWorkshopTools/WebAPI.cs
Tobias K f265f7e773 Replace mod text box by separate tab with workshop support (#263)
* Implement ModList tab which fetches and displays mod information from the workshop.

* ModListEditor: Implement drag and drop ordering, adding, removing and saving.

* Add SteamWorkshopService to VCS

* Add missing file to SteamworkshopService project.

* ModlistControl: Implement checkbox for hiding/showing dependency mods
disable until config is loaded.
design improvements.

* Add documentation for the new classes.

* Comply to naming conventions.

* Update Torch.Server.csproj

* Fix Mod.IsDependency not being serialized when saving

* Remove superfluous update of mod meta data.
Remove commented section in ConfigControl.xaml.

* Optimized SteamworkshopService according to commit review.

* Move SteamWorkshopService to Torch.Utils.SteamworkshopTools

* Remove debug output.

* Don't break stack trace with custom exception in SteamWorkshopTools.

* User ViewModel base class for ModItemInfo instead of implementing INotifyProperty directly.

* Wrap ModListControl in ScrollViewer.

* Rename SteamWorkshopTools utility to WebAPI.

* Revert steamkit call to use dynamic typing for clarity :/

* Mark webAPI based method for downloading workshop content as obsolete.

* Update Torch project definition.

* Disable building Torch client

* Update readme

* Change init order to ensure paths are initialized for plugins

* Reorder exception logging to reduce duplication

* Use thread safe queues in MtObservableCollectionBase

* Revert "Change init order to ensure paths are initialized for plugins"

This reverts commit 3f803b8107.

* Fix layout of ModListControl

* Combine Invokes to reduce allocations

* Replace string comparisons by string.Equals / string.IsNullOrEmpty

* Replace string comparisons by string.Equals / string.IsNullOrEmpty

* Use MtObservableList for Modlist to avoid race conditions.
2019-02-02 06:26:55 -05:00

292 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Net;
using NLog;
using SteamKit2;
using System.Net.Http;
namespace Torch.Utils.SteamWorkshopTools
{
public class WebAPI
{
private static Logger Log = LogManager.GetLogger("SteamWorkshopService");
public const uint AppID = 244850U;
public string Username { get; private set; }
private string password;
public bool IsReady { get; private set; }
public bool IsRunning { get; private set; }
private TaskCompletionSource<bool> logonTaskCompletionSource;
private SteamClient steamClient;
private CallbackManager cbManager;
private SteamUser steamUser;
private static WebAPI _instance;
public static WebAPI Instance
{
get
{
return _instance ?? (_instance = new WebAPI());
}
}
private WebAPI()
{
steamClient = new SteamClient();
cbManager = new CallbackManager(steamClient);
IsRunning = true;
}
public async Task<bool> Logon(string user = "anonymous", string pw = "")
{
if (string.IsNullOrEmpty(user))
throw new ArgumentNullException("User can't be null!");
if (!user.Equals("anonymous") && !pw.Equals(""))
throw new ArgumentNullException("Password can't be null if user is not anonymous!");
Username = user;
password = pw;
logonTaskCompletionSource = new TaskCompletionSource<bool>();
steamUser = steamClient.GetHandler<SteamUser>();
cbManager.Subscribe<SteamClient.ConnectedCallback>(OnConnected);
cbManager.Subscribe<SteamClient.DisconnectedCallback>(OnDisconnected);
cbManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
cbManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
Log.Info("Connecting to Steam...");
steamClient.Connect();
await logonTaskCompletionSource.Task;
return logonTaskCompletionSource.Task.Result;
}
public void CancelLogon()
{
logonTaskCompletionSource?.SetCanceled();
}
public async Task<Dictionary<ulong, PublishedItemDetails>> GetPublishedFileDetails(IEnumerable<ulong> workshopIds)
{
//if (!IsReady)
// throw new Exception("SteamWorkshopService not initialized!");
using (dynamic remoteStorage = SteamKit2.WebAPI.GetInterface("ISteamRemoteStorage"))
{
KeyValue allFilesDetails = null ;
remoteStorage.Timeout = TimeSpan.FromSeconds(30);
allFilesDetails = await Task.Run(delegate {
try
{
return remoteStorage.GetPublishedFileDetails1(
itemcount: workshopIds.Count(),
publishedfileids: workshopIds,
method: HttpMethod.Post);
// var ifaceArgs = new Dictionary<string, string>();
// ifaceArgs["itemcount"] = workshopIds.Count().ToString();
// no idea if that formatting is correct - in fact I get a 404 response
// ifaceArgs["publishedfileids"] = string.Join(",", workshopIds);
// return remoteStorage.Call(HttpMethod.Post, "GetPublishedFileDetails", args: ifaceArgs);
}
catch (HttpRequestException e)
{
Log.Error($"Fetching File Details failed: {e.Message}");
return null;
}
});
if (allFilesDetails == null)
return null;
//fileDetails = remoteStorage.Call(HttpMethod.Post, "GetPublishedFileDetails", 1, new Dictionary<string, string>() { { "itemcount", workshopIds.Count().ToString() }, { "publishedfileids", workshopIds.ToString() } });
var detailsList = allFilesDetails?.Children.Find((KeyValue kv) => kv.Name == "publishedfiledetails")?.Children;
var resultCount = allFilesDetails?.GetValueOrDefault<int>("resultcount");
if( detailsList == null || resultCount == null)
{
Log.Error("Received invalid data: ");
#if DEBUG
if(allFilesDetails != null)
PrintKeyValue(allFilesDetails);
return null;
#endif
}
if ( detailsList.Count != workshopIds.Count() || resultCount != workshopIds.Count())
{
Log.Error($"Received unexpected number of fileDetails. Expected: {workshopIds.Count()}, Received: {resultCount}");
return null;
}
var result = new Dictionary<ulong, PublishedItemDetails>();
for( int i = 0; i < resultCount; i++ )
{
var fileDetails = detailsList[i];
var tagContainer = fileDetails.Children.Find(item => item.Name == "tags");
List<string> tags = new List<string>();
if (tagContainer != null)
foreach (var tagKv in tagContainer.Children)
{
var tag = tagKv.Children.Find(item => item.Name == "tag")?.Value;
if( tag != null)
tags.Add(tag);
}
var publishedFileId = fileDetails.GetValueOrDefault<ulong>("publishedfileid");
result[publishedFileId] = new PublishedItemDetails()
{
PublishedFileId = publishedFileId,
Views = fileDetails.GetValueOrDefault<uint>("views"),
Subscriptions = fileDetails.GetValueOrDefault<uint>("subscriptions"),
TimeUpdated = DateTimeOffset.FromUnixTimeSeconds(fileDetails.GetValueOrDefault<long>("time_updated")).DateTime,
TimeCreated = DateTimeOffset.FromUnixTimeSeconds(fileDetails.GetValueOrDefault<long>("time_created")).DateTime,
Description = fileDetails.GetValueOrDefault<string>("description"),
Title = fileDetails.GetValueOrDefault<string>("title"),
FileUrl = fileDetails.GetValueOrDefault<string>("file_url"),
FileSize = fileDetails.GetValueOrDefault<long>("file_size"),
FileName = fileDetails.GetValueOrDefault<string>("filename"),
ConsumerAppId = fileDetails.GetValueOrDefault<ulong>("consumer_app_id"),
CreatorAppId = fileDetails.GetValueOrDefault<ulong>("creator_app_id"),
Creator = fileDetails.GetValueOrDefault<ulong>("creator"),
Tags = tags.ToArray()
};
}
return result;
}
}
[Obsolete("Space Engineers has transitioned to Steam's UGC api, therefore this method might not always work!")]
public async Task DownloadPublishedFile(PublishedItemDetails fileDetails, string dir, string name = null)
{
var fullPath = Path.Combine(dir, name);
if (name == null)
name = fileDetails.FileName;
var expectedSize = (fileDetails.FileSize == 0) ? -1 : fileDetails.FileSize;
using (var client = new WebClient())
{
try
{
var downloadTask = client.DownloadFileTaskAsync(fileDetails.FileUrl, Path.Combine(dir, name));
DateTime start = DateTime.Now;
for (int i = 0; i < 30; i++)
{
await Task.Delay(1000);
if (downloadTask.IsCompleted)
break;
}
if ( !downloadTask.IsCompleted )
{
client.CancelAsync();
throw new Exception("Timeout while attempting to downloading published workshop item!");
}
//var text = await client.DownloadStringTaskAsync(url);
//File.WriteAllText(fullPath, text);
}
catch (Exception e)
{
Log.Error("Failed to download workshop item! /n" +
$"{e.Message} - url: {fileDetails.FileUrl}, path: {Path.Combine(dir, name)}");
throw e;
}
}
}
class Printable
{
public KeyValue Data;
public int Offset;
public void Print()
{
Log.Info($"{new string(' ', Offset)}{Data.Name}: {Data.Value}");
}
}
private static void PrintKeyValue(KeyValue data)
{
var dataSet = new Stack<Printable>();
dataSet.Push(new Printable()
{
Data = data,
Offset = 0
});
while (dataSet.Count != 0)
{
var printable = dataSet.Pop();
foreach (var child in printable.Data.Children)
dataSet.Push(new Printable()
{
Data = child,
Offset = printable.Offset + 2
});
printable.Print();
}
}
#region CALLBACKS
private void OnConnected( SteamClient.ConnectedCallback callback)
{
Log.Info("Connected to Steam! Logging in '{0}'...", Username);
if( Username == "anonymous" )
steamUser.LogOnAnonymous();
else
steamUser.LogOn(new SteamUser.LogOnDetails
{
Username = Username,
Password = password
});
}
private void OnDisconnected( SteamClient.DisconnectedCallback callback )
{
Log.Info("Disconnected from Steam");
IsReady = false;
IsRunning = false;
}
private void OnLoggedOn( SteamUser.LoggedOnCallback callback )
{
if( callback.Result != EResult.OK )
{
string msg;
if( callback.Result == EResult.AccountLogonDenied )
{
msg = "Unable to logon to Steam: This account is Steamguard protected.";
Log.Warn(msg);
logonTaskCompletionSource.SetException(new Exception(msg));
IsRunning = false;
return;
}
msg = $"Unable to logon to Steam: {callback.Result} / {callback.ExtendedResult}";
Log.Warn(msg);
logonTaskCompletionSource.SetException(new Exception(msg));
IsRunning = false;
return;
}
IsReady = true;
Log.Info("Successfully logged on!");
logonTaskCompletionSource.SetResult(true);
}
private void OnLoggedOff( SteamUser.LoggedOffCallback callback )
{
IsReady = false;
Log.Info($"Logged off of Steam: {callback.Result}");
}
#endregion
}
}