CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user