Files
CVRManager/Services/SteamLink.cs
T
Exterrata 9bb4f85802 CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 02:46:35 -04:00

147 lines
5.5 KiB
C#

using SteamKit2;
using SteamKit2.Authentication;
namespace CVRManager.Services;
public sealed class SteamLink : IAsyncDisposable
{
readonly CallbackManager manager;
readonly CancellationTokenSource stop = new();
readonly TaskCompletionSource connected = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource<SteamUser.LoggedOnCallback>? loggedOn;
Task pump = Task.CompletedTask;
public SteamClient Client { get; } = new();
public SteamUser User { get; }
public SteamApps Apps { get; }
public SteamContent Content { get; }
public bool IsAnonymous { get; private set; }
readonly uint loginId = (uint)Random.Shared.NextInt64(0x10000, uint.MaxValue);
SteamLink()
{
manager = new CallbackManager(Client);
User = Client.GetHandler<SteamUser>()!;
Apps = Client.GetHandler<SteamApps>()!;
Content = Client.GetHandler<SteamContent>()!;
manager.Subscribe<SteamClient.ConnectedCallback>(_ => connected.TrySetResult());
manager.Subscribe<SteamClient.DisconnectedCallback>(_ =>
{
var e = new InvalidOperationException("Lost the connection to Steam.");
connected.TrySetException(e);
loggedOn?.TrySetException(e);
});
manager.Subscribe<SteamUser.LoggedOnCallback>(cb => loggedOn?.TrySetResult(cb));
}
public static async Task<SteamLink> ConnectAsync(CancellationToken ct)
{
var link = new SteamLink();
link.pump = Task.Run(() =>
{
while (!link.stop.IsCancellationRequested) link.manager.RunWaitCallbacks(TimeSpan.FromMilliseconds(200));
});
link.Client.Connect();
try
{
await WaitAsync(link.connected.Task, TimeSpan.FromSeconds(20), "Could not reach Steam.", ct);
return link;
}
catch
{
await link.DisposeAsync();
throw;
}
}
public static async Task<SteamLink> OpenAsync(string account, string refreshToken, CancellationToken ct)
{
var link = await ConnectAsync(ct);
try
{
if (string.IsNullOrEmpty(refreshToken)) await link.LogOnAnonymousAsync(ct);
else await link.LogOnAsync(account, refreshToken, ct);
return link;
}
catch
{
await link.DisposeAsync();
throw;
}
}
public async Task LogOnAnonymousAsync(CancellationToken ct)
{
IsAnonymous = true;
loggedOn = new(TaskCreationOptions.RunContinuationsAsynchronously);
User.LogOnAnonymous();
Check(await WaitAsync(loggedOn.Task, TimeSpan.FromSeconds(20), "Steam did not answer the login.", ct));
}
public async Task LogOnAsync(string account, string refreshToken, CancellationToken ct)
{
loggedOn = new(TaskCreationOptions.RunContinuationsAsynchronously);
User.LogOn(new SteamUser.LogOnDetails { Username = account, AccessToken = refreshToken, ShouldRememberPassword = true, LoginID = loginId });
Check(await WaitAsync(loggedOn.Task, TimeSpan.FromSeconds(20), "Steam did not answer the login.", ct));
}
static void Check(SteamUser.LoggedOnCallback cb)
{
if (cb.Result != EResult.OK)
throw new InvalidOperationException(cb.Result is EResult.AccessDenied or EResult.InvalidPassword or EResult.Expired or EResult.LoggedInElsewhere
? $"Steam refused the saved sign-in ({cb.Result}); sign in again."
: $"Steam login failed: {cb.Result}");
}
public static async Task<string> SignInAsync(
string account, string password, Action onDeviceConfirmation, Func<bool, Task<string>> deviceCode, Func<string, bool, Task<string>> emailCode, CancellationToken ct)
{
await using var link = await ConnectAsync(ct);
var session = await link.Client.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
{
Username = account,
Password = password,
IsPersistentSession = true,
DeviceFriendlyName = "CVRManager",
Authenticator = new Authenticator(onDeviceConfirmation, deviceCode, emailCode),
});
var result = await session.PollingWaitForResultAsync(ct);
return result.RefreshToken;
}
sealed class Authenticator(Action confirmation, Func<bool, Task<string>> device, Func<string, bool, Task<string>> email) : IAuthenticator
{
public Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect) => device(previousCodeWasIncorrect);
public Task<string> GetEmailCodeAsync(string emailAddress, bool previousCodeWasIncorrect) => email(emailAddress, previousCodeWasIncorrect);
public Task<bool> AcceptDeviceConfirmationAsync()
{
confirmation();
return Task.FromResult(true);
}
}
static async Task<T> WaitAsync<T>(Task<T> task, TimeSpan timeout, string message, CancellationToken ct)
{
var delay = Task.Delay(timeout, ct);
if (await Task.WhenAny(task, delay) == delay) throw new TimeoutException(message);
return await task;
}
static async Task WaitAsync(Task task, TimeSpan timeout, string message, CancellationToken ct)
{
var delay = Task.Delay(timeout, ct);
if (await Task.WhenAny(task, delay) == delay) throw new TimeoutException(message);
await task;
}
public async ValueTask DisposeAsync()
{
stop.Cancel();
try { Client.Disconnect(); } catch { }
await pump;
stop.Dispose();
}
}