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? 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()!; Apps = Client.GetHandler()!; Content = Client.GetHandler()!; manager.Subscribe(_ => connected.TrySetResult()); manager.Subscribe(_ => { var e = new InvalidOperationException("Lost the connection to Steam."); connected.TrySetException(e); loggedOn?.TrySetException(e); }); manager.Subscribe(cb => loggedOn?.TrySetResult(cb)); } public static async Task 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 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 SignInAsync( string account, string password, Action onDeviceConfirmation, Func> deviceCode, Func> 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> device, Func> email) : IAuthenticator { public Task GetDeviceCodeAsync(bool previousCodeWasIncorrect) => device(previousCodeWasIncorrect); public Task GetEmailCodeAsync(string emailAddress, bool previousCodeWasIncorrect) => email(emailAddress, previousCodeWasIncorrect); public Task AcceptDeviceConfirmationAsync() { confirmation(); return Task.FromResult(true); } } 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); 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(); } }