using System; using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Octokit; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.CivitTRPC; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Services; using ApiException = Refit.ApiException; namespace StabilityMatrix.Avalonia.Services; [Singleton(typeof(IAccountsService))] public class AccountsService : IAccountsService { private readonly ILogger logger; private readonly ISecretsManager secretsManager; private readonly ILykosAuthApi lykosAuthApi; private readonly ICivitTRPCApi civitTRPCApi; /// public event EventHandler? LykosAccountStatusUpdate; /// public event EventHandler? CivitAccountStatusUpdate; public LykosAccountStatusUpdateEventArgs? LykosStatus { get; private set; } public CivitAccountStatusUpdateEventArgs? CivitStatus { get; private set; } public AccountsService( ILogger logger, ISecretsManager secretsManager, ILykosAuthApi lykosAuthApi, ICivitTRPCApi civitTRPCApi ) { this.logger = logger; this.secretsManager = secretsManager; this.lykosAuthApi = lykosAuthApi; this.civitTRPCApi = civitTRPCApi; // Update our own status when the Lykos account status changes LykosAccountStatusUpdate += (_, args) => LykosStatus = args; } public async Task LykosLoginAsync(string email, string password) { var secrets = await secretsManager.SafeLoadAsync(); var tokens = await lykosAuthApi.PostLogin(new PostLoginRequest(email, password)); secrets = secrets with { LykosAccount = tokens }; await secretsManager.SaveAsync(secrets); await RefreshLykosAsync(secrets); } public async Task LykosSignupAsync(string email, string password, string username) { var secrets = await secretsManager.SafeLoadAsync(); var tokens = await lykosAuthApi.PostAccount( new PostAccountRequest(email, password, password, username) ); secrets = secrets with { LykosAccount = tokens }; await secretsManager.SaveAsync(secrets); await RefreshLykosAsync(secrets); } public async Task LykosLogoutAsync() { var secrets = await secretsManager.SafeLoadAsync(); await secretsManager.SaveAsync(secrets with { LykosAccount = null }); OnLykosAccountStatusUpdate(LykosAccountStatusUpdateEventArgs.Disconnected); } /// public async Task LykosPatreonOAuthLogoutAsync() { var secrets = await secretsManager.SafeLoadAsync(); if (secrets.LykosAccount is null) { throw new InvalidOperationException( "Lykos account must be connected in to manage OAuth connections" ); } await lykosAuthApi.DeletePatreonOAuth(); await RefreshLykosAsync(secrets); } public async Task CivitLoginAsync(string apiToken) { var secrets = await secretsManager.SafeLoadAsync(); // Get id first using the api token var userAccount = await civitTRPCApi.GetUserAccountDefault(apiToken); var id = userAccount.Result.Data.Json.Id; // Then get the username using the id var account = await civitTRPCApi.GetUserById( new CivitGetUserByIdRequest { Id = id }, apiToken ); var username = account.Result.Data.Json.Username; secrets = secrets with { CivitApi = new CivitApiTokens(apiToken, username) }; await secretsManager.SaveAsync(secrets); await RefreshCivitAsync(secrets); } /// public async Task CivitLogoutAsync() { var secrets = await secretsManager.SafeLoadAsync(); await secretsManager.SaveAsync(secrets with { CivitApi = null }); OnCivitAccountStatusUpdate(CivitAccountStatusUpdateEventArgs.Disconnected); } public async Task RefreshAsync() { var secrets = await secretsManager.SafeLoadAsync(); await RefreshLykosAsync(secrets); await RefreshCivitAsync(secrets); } private async Task RefreshLykosAsync(Secrets secrets) { if ( secrets.LykosAccount is not null && !string.IsNullOrWhiteSpace(secrets.LykosAccount?.RefreshToken) && !string.IsNullOrWhiteSpace(secrets.LykosAccount?.AccessToken) ) { try { var user = await lykosAuthApi.GetUserSelf(); OnLykosAccountStatusUpdate( new LykosAccountStatusUpdateEventArgs { IsConnected = true, User = user } ); return; } catch (OperationCanceledException) { logger.LogWarning("Timed out while fetching Lykos Auth user info"); } catch (InvalidOperationException e) { logger.LogWarning(e, "Failed to get authentication token"); } catch (ApiException e) { if (e.StatusCode is HttpStatusCode.Unauthorized) { } else { logger.LogWarning(e, "Failed to get user info from Lykos"); } } catch (Exception e) { logger.LogError(e, "Unknown error while refreshing Lykos account status"); } } OnLykosAccountStatusUpdate(LykosAccountStatusUpdateEventArgs.Disconnected); } private async Task RefreshCivitAsync(Secrets secrets) { if (secrets.CivitApi is not null) { try { var user = await civitTRPCApi.GetUserProfile( new CivitUserProfileRequest { Username = secrets.CivitApi.Username }, secrets.CivitApi.ApiToken ); OnCivitAccountStatusUpdate( new CivitAccountStatusUpdateEventArgs { IsConnected = true, UserProfile = user } ); return; } catch (OperationCanceledException) { logger.LogWarning("Timed out while fetching Civit Auth user info"); } catch (ApiException e) { if (e.StatusCode is HttpStatusCode.Unauthorized) { } else { logger.LogWarning(e, "Failed to get user info from Civit"); } } } OnCivitAccountStatusUpdate(CivitAccountStatusUpdateEventArgs.Disconnected); } private void OnLykosAccountStatusUpdate(LykosAccountStatusUpdateEventArgs e) { if (!e.IsConnected && LykosStatus?.IsConnected == true) { logger.LogInformation("Lykos account disconnected"); } else if (e.IsConnected && LykosStatus?.IsConnected == false) { logger.LogInformation( "Lykos account connected: {Id} ({Username})", e.User?.Id, e.User?.Account.Name ); } LykosAccountStatusUpdate?.Invoke(this, e); } private void OnCivitAccountStatusUpdate(CivitAccountStatusUpdateEventArgs e) { if (!e.IsConnected && CivitStatus?.IsConnected == true) { logger.LogInformation("Civit account disconnected"); } else if (e.IsConnected && CivitStatus?.IsConnected == false) { logger.LogInformation( "Civit account connected: {Id} ({Username})", e.UserProfile?.UserId, e.UserProfile?.Username ); } CivitAccountStatusUpdate?.Invoke(this, e); } }