2025-08-22 02:19:48 +01:00
using Dalamud.Utility ;
using MareSynchronos.API.Data ;
using MareSynchronos.API.Data.Extensions ;
using MareSynchronos.API.Dto ;
using MareSynchronos.API.Dto.User ;
using MareSynchronos.API.SignalR ;
using MareSynchronos.MareConfiguration ;
using MareSynchronos.MareConfiguration.Models ;
using MareSynchronos.PlayerData.Pairs ;
using MareSynchronos.Services ;
using MareSynchronos.Services.Mediator ;
using MareSynchronos.Services.ServerConfiguration ;
using MareSynchronos.WebAPI.SignalR ;
using MareSynchronos.WebAPI.SignalR.Utils ;
using Microsoft.AspNetCore.SignalR.Client ;
using Microsoft.Extensions.Logging ;
using System.Reflection ;
namespace MareSynchronos.WebAPI ;
#pragma warning disable MA0040
public sealed partial class ApiController : DisposableMediatorSubscriberBase , IMareHubClient
{
2025-08-22 21:26:51 +01:00
public const string SnowcloakServer = "Snowcloak Main Server" ;
public const string SnowcloakServiceUri = "wss://hub.snowcloak-sync.com" ;
public const string SnowcloakServiceApiUri = "wss://hub.snowcloak-sync.com/" ;
public const string SnowcloakServiceHubUri = "wss://hub.snowcloak-sync.com/mare" ;
2025-08-22 02:19:48 +01:00
private readonly DalamudUtilService _dalamudUtil ;
private readonly HubFactory _hubFactory ;
private readonly PairManager _pairManager ;
private readonly ServerConfigurationManager _serverManager ;
private readonly TokenProvider _tokenProvider ;
private CancellationTokenSource _connectionCancellationTokenSource ;
private ConnectionDto ? _connectionDto ;
private bool _doNotNotifyOnNextInfo = false ;
private CancellationTokenSource ? _healthCheckTokenSource = new ( ) ;
private bool _initialized ;
private HubConnection ? _mareHub ;
private ServerState _serverState ;
private CensusUpdateMessage ? _lastCensus ;
public ApiController ( ILogger < ApiController > logger , HubFactory hubFactory , DalamudUtilService dalamudUtil ,
PairManager pairManager , ServerConfigurationManager serverManager , MareMediator mediator ,
TokenProvider tokenProvider ) : base ( logger , mediator )
{
_hubFactory = hubFactory ;
_dalamudUtil = dalamudUtil ;
_pairManager = pairManager ;
_serverManager = serverManager ;
_tokenProvider = tokenProvider ;
_connectionCancellationTokenSource = new CancellationTokenSource ( ) ;
Mediator . Subscribe < DalamudLoginMessage > ( this , ( _ ) = > DalamudUtilOnLogIn ( ) ) ;
Mediator . Subscribe < DalamudLogoutMessage > ( this , ( _ ) = > DalamudUtilOnLogOut ( ) ) ;
Mediator . Subscribe < HubClosedMessage > ( this , ( msg ) = > MareHubOnClosed ( msg . Exception ) ) ;
Mediator . Subscribe < HubReconnectedMessage > ( this , ( msg ) = > _ = MareHubOnReconnected ( ) ) ;
Mediator . Subscribe < HubReconnectingMessage > ( this , ( msg ) = > MareHubOnReconnecting ( msg . Exception ) ) ;
Mediator . Subscribe < CyclePauseMessage > ( this , ( msg ) = > _ = CyclePause ( msg . UserData ) ) ;
Mediator . Subscribe < CensusUpdateMessage > ( this , ( msg ) = > _lastCensus = msg ) ;
Mediator . Subscribe < PauseMessage > ( this , ( msg ) = > _ = Pause ( msg . UserData ) ) ;
ServerState = ServerState . Offline ;
if ( _dalamudUtil . IsLoggedIn )
{
DalamudUtilOnLogIn ( ) ;
}
}
public string AuthFailureMessage { get ; private set ; } = string . Empty ;
public Version CurrentClientVersion = > _connectionDto ? . CurrentClientVersion ? ? new Version ( 0 , 0 , 0 ) ;
public string DisplayName = > _connectionDto ? . User . AliasOrUID ? ? string . Empty ;
public bool IsConnected = > ServerState = = ServerState . Connected ;
public bool IsCurrentVersion = > ( Assembly . GetExecutingAssembly ( ) . GetName ( ) . Version ? ? new Version ( 0 , 0 , 0 , 0 ) ) > = ( _connectionDto ? . CurrentClientVersion ? ? new Version ( 0 , 0 , 0 , 0 ) ) ;
public int OnlineUsers = > SystemInfoDto . OnlineUsers ;
public bool ServerAlive = > ServerState is ServerState . Connected or ServerState . RateLimited or ServerState . Unauthorized or ServerState . Disconnected ;
public ServerInfo ServerInfo = > _connectionDto ? . ServerInfo ? ? new ServerInfo ( ) ;
public ServerState ServerState
{
get = > _serverState ;
private set
{
Logger . LogDebug ( "New ServerState: {value}, prev ServerState: {_serverState}" , value , _serverState ) ;
_serverState = value ;
}
}
public SystemInfoDto SystemInfoDto { get ; private set ; } = new ( ) ;
public string UID = > _connectionDto ? . User . UID ? ? string . Empty ;
public async Task < bool > CheckClientHealth ( )
{
return await _mareHub ! . InvokeAsync < bool > ( nameof ( CheckClientHealth ) ) . ConfigureAwait ( false ) ;
}
public async Task CreateConnections ( )
{
Logger . LogDebug ( "CreateConnections called" ) ;
if ( _serverManager . CurrentServer ? . FullPause ? ? true )
{
Logger . LogInformation ( "Not recreating Connection, paused" ) ;
_connectionDto = null ;
await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ;
_connectionCancellationTokenSource ? . Cancel ( ) ;
return ;
}
var secretKey = _serverManager . GetSecretKey ( out bool multi ) ;
if ( multi )
{
Logger . LogWarning ( "Multiple secret keys for current character" ) ;
_connectionDto = null ;
2025-09-01 11:54:02 -04:00
Mediator . Publish ( new NotificationMessage ( "Multiple Identical Characters detected" , "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect." ,
2025-08-22 02:19:48 +01:00
NotificationType . Error ) ) ;
await StopConnection ( ServerState . MultiChara ) . ConfigureAwait ( false ) ;
_connectionCancellationTokenSource ? . Cancel ( ) ;
return ;
}
if ( secretKey = = null )
{
Logger . LogWarning ( "No secret key set for current character" ) ;
_connectionDto = null ;
await StopConnection ( ServerState . NoSecretKey ) . ConfigureAwait ( false ) ;
_connectionCancellationTokenSource ? . Cancel ( ) ;
return ;
}
await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ;
Logger . LogInformation ( "Recreating Connection" ) ;
Mediator . Publish ( new EventMessage ( new Services . Events . Event ( nameof ( ApiController ) , Services . Events . EventSeverity . Informational ,
$"Starting Connection to {_serverManager.CurrentServer.ServerName}" ) ) ) ;
_connectionCancellationTokenSource ? . Cancel ( ) ;
_connectionCancellationTokenSource ? . Dispose ( ) ;
_connectionCancellationTokenSource = new CancellationTokenSource ( ) ;
var token = _connectionCancellationTokenSource . Token ;
while ( ServerState is not ServerState . Connected & & ! token . IsCancellationRequested )
{
AuthFailureMessage = string . Empty ;
await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ;
ServerState = ServerState . Connecting ;
try
{
Logger . LogDebug ( "Building connection" ) ;
try
{
await _tokenProvider . GetOrUpdateToken ( token ) . ConfigureAwait ( false ) ;
}
catch ( MareAuthFailureException ex )
{
AuthFailureMessage = ex . Reason ;
throw new HttpRequestException ( "Error during authentication" , ex , System . Net . HttpStatusCode . Unauthorized ) ;
}
while ( ! await _dalamudUtil . GetIsPlayerPresentAsync ( ) . ConfigureAwait ( false ) & & ! token . IsCancellationRequested )
{
Logger . LogDebug ( "Player not loaded in yet, waiting" ) ;
await Task . Delay ( TimeSpan . FromSeconds ( 1 ) , token ) . ConfigureAwait ( false ) ;
}
if ( token . IsCancellationRequested ) break ;
_mareHub = await _hubFactory . GetOrCreate ( token ) . ConfigureAwait ( false ) ;
InitializeApiHooks ( ) ;
await _mareHub . StartAsync ( token ) . ConfigureAwait ( false ) ;
_connectionDto = await GetConnectionDto ( ) . ConfigureAwait ( false ) ;
ServerState = ServerState . Connected ;
var currentClientVer = Assembly . GetExecutingAssembly ( ) . GetName ( ) . Version ! ;
if ( _connectionDto . ServerVersion ! = IMareHub . ApiVersion )
{
if ( _connectionDto . CurrentClientVersion > currentClientVer )
{
Mediator . Publish ( new NotificationMessage ( "Client incompatible" ,
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
2025-08-22 21:26:51 +01:00
$"This client version is incompatible and will not be able to connect. Please update your Snowcloak client." ,
2025-08-22 02:19:48 +01:00
NotificationType . Error ) ) ;
}
await StopConnection ( ServerState . VersionMisMatch ) . ConfigureAwait ( false ) ;
return ;
}
if ( _connectionDto . CurrentClientVersion > currentClientVer )
{
Mediator . Publish ( new NotificationMessage ( "Client outdated" ,
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
2025-08-22 21:26:51 +01:00
$"Please keep your Snowcloak client up-to-date." ,
2025-08-22 02:19:48 +01:00
NotificationType . Warning , TimeSpan . FromSeconds ( 15 ) ) ) ;
}
if ( _dalamudUtil . HasModifiedGameFiles )
{
Logger . LogWarning ( "Detected modified game files on connection" ) ;
#if false
Mediator . Publish ( new NotificationMessage ( "Modified Game Files detected" ,
"Dalamud has reported modified game files in your FFXIV installation. " +
"You will be able to connect, but the synchronization functionality might be (partially) broken. " +
"Exit the game and repair it through XIVLauncher to get rid of this message." ,
NotificationType . Error , TimeSpan . FromSeconds ( 15 ) ) ) ;
#endif
}
await LoadIninitialPairs ( ) . ConfigureAwait ( false ) ;
await LoadOnlinePairs ( ) . ConfigureAwait ( false ) ;
}
catch ( OperationCanceledException )
{
Logger . LogWarning ( "Connection attempt cancelled" ) ;
return ;
}
catch ( HttpRequestException ex )
{
Logger . LogWarning ( ex , "HttpRequestException on Connection" ) ;
if ( ex . StatusCode = = System . Net . HttpStatusCode . Unauthorized )
{
await StopConnection ( ServerState . Unauthorized ) . ConfigureAwait ( false ) ;
return ;
}
ServerState = ServerState . Reconnecting ;
Logger . LogInformation ( "Failed to establish connection, retrying" ) ;
await Task . Delay ( TimeSpan . FromSeconds ( new Random ( ) . Next ( 5 , 20 ) ) , token ) . ConfigureAwait ( false ) ;
}
catch ( InvalidOperationException ex )
{
Logger . LogWarning ( ex , "InvalidOperationException on connection" ) ;
await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ;
return ;
}
catch ( Exception ex )
{
Logger . LogWarning ( ex , "Exception on Connection" ) ;
Logger . LogInformation ( "Failed to establish connection, retrying" ) ;
await Task . Delay ( TimeSpan . FromSeconds ( new Random ( ) . Next ( 5 , 20 ) ) , token ) . ConfigureAwait ( false ) ;
}
}
}
public Task CyclePause ( UserData userData )
{
CancellationTokenSource cts = new ( ) ;
cts . CancelAfter ( TimeSpan . FromSeconds ( 5 ) ) ;
_ = Task . Run ( async ( ) = >
{
var pair = _pairManager . GetOnlineUserPairs ( ) . Single ( p = > p . UserPair ! = null & & p . UserData = = userData ) ;
var perm = pair . UserPair ! . OwnPermissions ;
perm . SetPaused ( paused : true ) ;
await UserSetPairPermissions ( new UserPermissionsDto ( userData , perm ) ) . ConfigureAwait ( false ) ;
// wait until it's changed
while ( pair . UserPair ! . OwnPermissions ! = perm )
{
await Task . Delay ( 250 , cts . Token ) . ConfigureAwait ( false ) ;
Logger . LogTrace ( "Waiting for permissions change for {data}" , userData ) ;
}
perm . SetPaused ( paused : false ) ;
await UserSetPairPermissions ( new UserPermissionsDto ( userData , perm ) ) . ConfigureAwait ( false ) ;
} , cts . Token ) . ContinueWith ( ( t ) = > cts . Dispose ( ) ) ;
return Task . CompletedTask ;
}
public async Task Pause ( UserData userData )
{
var pair = _pairManager . GetOnlineUserPairs ( ) . Single ( p = > p . UserPair ! = null & & p . UserData = = userData ) ;
var perm = pair . UserPair ! . OwnPermissions ;
perm . SetPaused ( paused : true ) ;
await UserSetPairPermissions ( new UserPermissionsDto ( userData , perm ) ) . ConfigureAwait ( false ) ;
}
public Task < ConnectionDto > GetConnectionDto ( ) = > GetConnectionDto ( true ) ;
public async Task < ConnectionDto > GetConnectionDto ( bool publishConnected = true )
{
var dto = await _mareHub ! . InvokeAsync < ConnectionDto > ( nameof ( GetConnectionDto ) ) . ConfigureAwait ( false ) ;
if ( publishConnected ) Mediator . Publish ( new ConnectedMessage ( dto ) ) ;
return dto ;
}
protected override void Dispose ( bool disposing )
{
base . Dispose ( disposing ) ;
_healthCheckTokenSource ? . Cancel ( ) ;
_ = Task . Run ( async ( ) = > await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ) ;
_connectionCancellationTokenSource ? . Cancel ( ) ;
}
private async Task ClientHealthCheck ( CancellationToken ct )
{
while ( ! ct . IsCancellationRequested & & _mareHub ! = null )
{
await Task . Delay ( TimeSpan . FromSeconds ( 30 ) , ct ) . ConfigureAwait ( false ) ;
Logger . LogDebug ( "Checking Client Health State" ) ;
_ = await CheckClientHealth ( ) . ConfigureAwait ( false ) ;
}
}
private void DalamudUtilOnLogIn ( )
{
_ = Task . Run ( ( ) = > CreateConnections ( ) ) ;
}
private void DalamudUtilOnLogOut ( )
{
_ = Task . Run ( async ( ) = > await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ) ;
ServerState = ServerState . Offline ;
}
private void InitializeApiHooks ( )
{
if ( _mareHub = = null ) return ;
Logger . LogDebug ( "Initializing data" ) ;
OnDownloadReady ( ( guid ) = > _ = Client_DownloadReady ( guid ) ) ;
OnReceiveServerMessage ( ( sev , msg ) = > _ = Client_ReceiveServerMessage ( sev , msg ) ) ;
OnUpdateSystemInfo ( ( dto ) = > _ = Client_UpdateSystemInfo ( dto ) ) ;
OnUserSendOffline ( ( dto ) = > _ = Client_UserSendOffline ( dto ) ) ;
OnUserAddClientPair ( ( dto ) = > _ = Client_UserAddClientPair ( dto ) ) ;
OnUserReceiveCharacterData ( ( dto ) = > _ = Client_UserReceiveCharacterData ( dto ) ) ;
OnUserRemoveClientPair ( dto = > _ = Client_UserRemoveClientPair ( dto ) ) ;
OnUserSendOnline ( dto = > _ = Client_UserSendOnline ( dto ) ) ;
OnUserUpdateOtherPairPermissions ( dto = > _ = Client_UserUpdateOtherPairPermissions ( dto ) ) ;
OnUserUpdateSelfPairPermissions ( dto = > _ = Client_UserUpdateSelfPairPermissions ( dto ) ) ;
OnUserReceiveUploadStatus ( dto = > _ = Client_UserReceiveUploadStatus ( dto ) ) ;
OnUserUpdateProfile ( dto = > _ = Client_UserUpdateProfile ( dto ) ) ;
OnGroupChangePermissions ( ( dto ) = > _ = Client_GroupChangePermissions ( dto ) ) ;
OnGroupDelete ( ( dto ) = > _ = Client_GroupDelete ( dto ) ) ;
OnGroupPairChangeUserInfo ( ( dto ) = > _ = Client_GroupPairChangeUserInfo ( dto ) ) ;
OnGroupPairJoined ( ( dto ) = > _ = Client_GroupPairJoined ( dto ) ) ;
OnGroupPairLeft ( ( dto ) = > _ = Client_GroupPairLeft ( dto ) ) ;
OnGroupSendFullInfo ( ( dto ) = > _ = Client_GroupSendFullInfo ( dto ) ) ;
OnGroupSendInfo ( ( dto ) = > _ = Client_GroupSendInfo ( dto ) ) ;
OnGroupPairChangePermissions ( ( dto ) = > _ = Client_GroupPairChangePermissions ( dto ) ) ;
OnUserChatMsg ( ( dto ) = > _ = Client_UserChatMsg ( dto ) ) ;
OnGroupChatMsg ( ( dto ) = > _ = Client_GroupChatMsg ( dto ) ) ;
OnGposeLobbyJoin ( ( dto ) = > _ = Client_GposeLobbyJoin ( dto ) ) ;
OnGposeLobbyLeave ( ( dto ) = > _ = Client_GposeLobbyLeave ( dto ) ) ;
OnGposeLobbyPushCharacterData ( ( dto ) = > _ = Client_GposeLobbyPushCharacterData ( dto ) ) ;
OnGposeLobbyPushPoseData ( ( dto , data ) = > _ = Client_GposeLobbyPushPoseData ( dto , data ) ) ;
OnGposeLobbyPushWorldData ( ( dto , data ) = > _ = Client_GposeLobbyPushWorldData ( dto , data ) ) ;
_healthCheckTokenSource ? . Cancel ( ) ;
_healthCheckTokenSource ? . Dispose ( ) ;
_healthCheckTokenSource = new CancellationTokenSource ( ) ;
_ = ClientHealthCheck ( _healthCheckTokenSource . Token ) ;
_initialized = true ;
}
private async Task LoadIninitialPairs ( )
{
foreach ( var userPair in await UserGetPairedClients ( ) . ConfigureAwait ( false ) )
{
Logger . LogDebug ( "Individual Pair: {userPair}" , userPair ) ;
_pairManager . AddUserPair ( userPair , addToLastAddedUser : false ) ;
}
foreach ( var entry in await GroupsGetAll ( ) . ConfigureAwait ( false ) )
{
Logger . LogDebug ( "Group: {entry}" , entry ) ;
_pairManager . AddGroup ( entry ) ;
}
foreach ( var group in _pairManager . GroupPairs . Keys )
{
var users = await GroupsGetUsersInGroup ( group ) . ConfigureAwait ( false ) ;
foreach ( var user in users )
{
Logger . LogDebug ( "Group Pair: {user}" , user ) ;
_pairManager . AddGroupPair ( user ) ;
}
}
}
private async Task LoadOnlinePairs ( )
{
foreach ( var entry in await UserGetOnlinePairs ( ) . ConfigureAwait ( false ) )
{
Logger . LogDebug ( "Pair online: {pair}" , entry ) ;
_pairManager . MarkPairOnline ( entry , sendNotif : false ) ;
}
}
private void MareHubOnClosed ( Exception ? arg )
{
_healthCheckTokenSource ? . Cancel ( ) ;
Mediator . Publish ( new DisconnectedMessage ( ) ) ;
ServerState = ServerState . Offline ;
if ( arg ! = null )
{
Logger . LogWarning ( arg , "Connection closed" ) ;
}
else
{
Logger . LogInformation ( "Connection closed" ) ;
}
}
private async Task MareHubOnReconnected ( )
{
ServerState = ServerState . Reconnecting ;
try
{
InitializeApiHooks ( ) ;
_connectionDto = await GetConnectionDto ( publishConnected : false ) . ConfigureAwait ( false ) ;
if ( _connectionDto . ServerVersion ! = IMareHub . ApiVersion )
{
await StopConnection ( ServerState . VersionMisMatch ) . ConfigureAwait ( false ) ;
return ;
}
ServerState = ServerState . Connected ;
await LoadIninitialPairs ( ) . ConfigureAwait ( false ) ;
await LoadOnlinePairs ( ) . ConfigureAwait ( false ) ;
Mediator . Publish ( new ConnectedMessage ( _connectionDto ) ) ;
}
catch ( Exception ex )
{
Logger . LogCritical ( ex , "Failure to obtain data after reconnection" ) ;
await StopConnection ( ServerState . Disconnected ) . ConfigureAwait ( false ) ;
}
}
private void MareHubOnReconnecting ( Exception ? arg )
{
_doNotNotifyOnNextInfo = true ;
_healthCheckTokenSource ? . Cancel ( ) ;
ServerState = ServerState . Reconnecting ;
Logger . LogWarning ( arg , "Connection closed... Reconnecting" ) ;
Mediator . Publish ( new EventMessage ( new Services . Events . Event ( nameof ( ApiController ) , Services . Events . EventSeverity . Warning ,
$"Connection interrupted, reconnecting to {_serverManager.CurrentServer.ServerName}" ) ) ) ;
}
private async Task StopConnection ( ServerState state )
{
ServerState = ServerState . Disconnecting ;
Logger . LogInformation ( "Stopping existing connection" ) ;
await _hubFactory . DisposeHubAsync ( ) . ConfigureAwait ( false ) ;
if ( _mareHub is not null )
{
Mediator . Publish ( new EventMessage ( new Services . Events . Event ( nameof ( ApiController ) , Services . Events . EventSeverity . Informational ,
$"Stopping existing connection to {_serverManager.CurrentServer.ServerName}" ) ) ) ;
_initialized = false ;
_healthCheckTokenSource ? . Cancel ( ) ;
Mediator . Publish ( new DisconnectedMessage ( ) ) ;
_mareHub = null ;
_connectionDto = null ;
}
ServerState = state ;
}
}
#pragma warning restore MA0040