All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2.0.0 Changes: - Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more. - Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name. - Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much. - Chat has been added to the top menu, working in Zone or in Syncshells to be used there. - Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well. - Moved to the internal object table to have faster load times for users; people should load in faster - Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files - Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore. - Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all). - Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list. - Lightfinder plates have been moved away from using Nameplates, but will use an overlay. - Main UI has been changed a bit with a gradient, and on hover will glow up now. - Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items. - Reworked Settings UI to look more modern. - Performance should be better due to new systems that would dispose of the collections and better caching of items. Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: choco <choco@patat.nl> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Minmoose <KennethBohr@outlook.com> Reviewed-on: #92
767 lines
33 KiB
C#
767 lines
33 KiB
C#
using System.Reflection;
|
|
using Dalamud.Utility;
|
|
using LightlessSync.API.Data;
|
|
using LightlessSync.API.Data.Extensions;
|
|
using LightlessSync.API.Dto;
|
|
using LightlessSync.API.Dto.Chat;
|
|
using LightlessSync.API.Dto.User;
|
|
using LightlessSync.API.SignalR;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Models;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Services.ServerConfiguration;
|
|
using LightlessSync.WebAPI.SignalR;
|
|
using LightlessSync.WebAPI.SignalR.Utils;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LightlessSync.WebAPI;
|
|
|
|
#pragma warning disable MA0040
|
|
public sealed partial class ApiController : DisposableMediatorSubscriberBase, ILightlessHubClient
|
|
{
|
|
public const string MainServer = "Follow the light (Official Central Server)";
|
|
public const string MainServiceUri = "wss://sync.lightless-sync.org";
|
|
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
private readonly HubFactory _hubFactory;
|
|
private readonly PairCoordinator _pairCoordinator;
|
|
private readonly PairRequestService _pairRequestService;
|
|
private readonly ServerConfigurationManager _serverManager;
|
|
private readonly TokenProvider _tokenProvider;
|
|
private readonly LightlessConfigService _lightlessConfigService;
|
|
private CancellationTokenSource _connectionCancellationTokenSource;
|
|
private ConnectionDto? _connectionDto;
|
|
private bool _doNotNotifyOnNextInfo = false;
|
|
private CancellationTokenSource? _healthCheckTokenSource = new();
|
|
private bool _initialized;
|
|
private string? _lastUsedToken;
|
|
private HubConnection? _lightlessHub;
|
|
private ServerState _serverState;
|
|
private CensusUpdateMessage? _lastCensus;
|
|
private IReadOnlyList<ZoneChatChannelInfoDto> _zoneChatChannels = Array.Empty<ZoneChatChannelInfoDto>();
|
|
private IReadOnlyList<GroupChatChannelInfoDto> _groupChatChannels = Array.Empty<GroupChatChannelInfoDto>();
|
|
private event Action<ChatMessageDto>? ChatMessageReceived;
|
|
|
|
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
|
|
PairCoordinator pairCoordinator, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
|
|
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
|
|
{
|
|
_hubFactory = hubFactory;
|
|
_dalamudUtil = dalamudUtil;
|
|
_pairCoordinator = pairCoordinator;
|
|
_pairRequestService = pairRequestService;
|
|
_serverManager = serverManager;
|
|
_tokenProvider = tokenProvider;
|
|
_lightlessConfigService = lightlessConfigService;
|
|
_connectionCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
|
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
|
|
Mediator.Subscribe<HubClosedMessage>(this, (msg) => LightlessHubOnClosed(msg.Exception));
|
|
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = LightlessHubOnReconnectedAsync());
|
|
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => LightlessHubOnReconnecting(msg.Exception));
|
|
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePauseAsync(msg.Pair));
|
|
Mediator.Subscribe<CensusUpdateMessage>(this, (msg) => _lastCensus = msg);
|
|
Mediator.Subscribe<PauseMessage>(this, (msg) => _ = PauseAsync(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 DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
|
|
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
|
|
|
|
public bool HasVanity => _connectionDto?.HasVanity ?? false;
|
|
public string TextColorHex => _connectionDto?.TextColorHex ?? string.Empty;
|
|
public string TextGlowColorHex => _connectionDto?.TextGlowColorHex ?? 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 IReadOnlyList<ZoneChatChannelInfoDto> ZoneChatChannels => _zoneChatChannels;
|
|
public IReadOnlyList<GroupChatChannelInfoDto> GroupChatChannels => _groupChatChannels;
|
|
public string UID => _connectionDto?.User.UID ?? string.Empty;
|
|
|
|
public event Action? OnConnected;
|
|
|
|
public async Task<bool> CheckClientHealth()
|
|
{
|
|
var hub = _lightlessHub;
|
|
if (hub is null || !IsConnected)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
return await hub.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogDebug(ex, "Client health check failed.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task RefreshChatChannelsAsync()
|
|
{
|
|
if (_lightlessHub is null || !IsConnected)
|
|
return;
|
|
|
|
await Task.WhenAll(GetZoneChatChannelsAsync(), GetGroupChatChannelsAsync()).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<ZoneChatChannelInfoDto>> GetZoneChatChannelsAsync()
|
|
{
|
|
if (_lightlessHub is null || !IsConnected)
|
|
return _zoneChatChannels;
|
|
|
|
var channels = await _lightlessHub.InvokeAsync<IReadOnlyList<ZoneChatChannelInfoDto>>("GetZoneChatChannels").ConfigureAwait(false);
|
|
_zoneChatChannels = channels;
|
|
return channels;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<GroupChatChannelInfoDto>> GetGroupChatChannelsAsync()
|
|
{
|
|
if (_lightlessHub is null || !IsConnected)
|
|
return _groupChatChannels;
|
|
|
|
var channels = await _lightlessHub.InvokeAsync<IReadOnlyList<GroupChatChannelInfoDto>>("GetGroupChatChannels").ConfigureAwait(false);
|
|
_groupChatChannels = channels;
|
|
return channels;
|
|
}
|
|
|
|
Task<IReadOnlyList<ZoneChatChannelInfoDto>> ILightlessHub.GetZoneChatChannels()
|
|
=> _lightlessHub!.InvokeAsync<IReadOnlyList<ZoneChatChannelInfoDto>>("GetZoneChatChannels");
|
|
|
|
Task<IReadOnlyList<GroupChatChannelInfoDto>> ILightlessHub.GetGroupChatChannels()
|
|
=> _lightlessHub!.InvokeAsync<IReadOnlyList<GroupChatChannelInfoDto>>("GetGroupChatChannels");
|
|
|
|
public async Task CreateConnectionsAsync()
|
|
{
|
|
if (!_serverManager.ShownCensusPopup)
|
|
{
|
|
Mediator.Publish(new OpenCensusPopupMessage());
|
|
while (!_serverManager.ShownCensusPopup)
|
|
{
|
|
await Task.Delay(500).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
Logger.LogDebug("CreateConnections called");
|
|
|
|
if (_serverManager.CurrentServer?.FullPause ?? true)
|
|
{
|
|
Logger.LogInformation("Not recreating Connection, paused");
|
|
_connectionDto = null;
|
|
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!_serverManager.CurrentServer.UseOAuth2)
|
|
{
|
|
var secretKey = _serverManager.GetSecretKey(out bool multi);
|
|
if (multi)
|
|
{
|
|
Logger.LogWarning("Multiple secret keys for current character");
|
|
_connectionDto = null;
|
|
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 to Lightless.",
|
|
NotificationType.Error));
|
|
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (secretKey.IsNullOrEmpty())
|
|
{
|
|
Logger.LogWarning("No secret key set for current character");
|
|
_connectionDto = null;
|
|
await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false);
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var oauth2 = _serverManager.GetOAuth2(out bool multi);
|
|
if (multi)
|
|
{
|
|
Logger.LogWarning("Multiple secret keys for current character");
|
|
_connectionDto = null;
|
|
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 to Lightless.",
|
|
NotificationType.Error));
|
|
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!oauth2.HasValue)
|
|
{
|
|
Logger.LogWarning("No UID/OAuth set for current character");
|
|
_connectionDto = null;
|
|
await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false);
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!await _tokenProvider.TryUpdateOAuth2LoginTokenAsync(_serverManager.CurrentServer).ConfigureAwait(false))
|
|
{
|
|
Logger.LogWarning("OAuth2 login token could not be updated");
|
|
_connectionDto = null;
|
|
await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false);
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
await StopConnectionAsync(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}")));
|
|
|
|
if (_connectionCancellationTokenSource != null)
|
|
{
|
|
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
_connectionCancellationTokenSource?.Dispose();
|
|
_connectionCancellationTokenSource = new CancellationTokenSource();
|
|
var token = _connectionCancellationTokenSource.Token;
|
|
while (ServerState is not ServerState.Connected && !token.IsCancellationRequested)
|
|
{
|
|
AuthFailureMessage = string.Empty;
|
|
|
|
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
|
|
ServerState = ServerState.Connecting;
|
|
|
|
try
|
|
{
|
|
Logger.LogDebug("Building connection");
|
|
|
|
try
|
|
{
|
|
_lastUsedToken = await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false);
|
|
}
|
|
catch (LightlessAuthFailureException 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;
|
|
|
|
_lightlessHub = _hubFactory.GetOrCreate(token);
|
|
InitializeApiHooks();
|
|
|
|
await _lightlessHub.StartAsync(token).ConfigureAwait(false);
|
|
|
|
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
|
|
|
ServerState = ServerState.Connected;
|
|
OnConnected?.Invoke();
|
|
|
|
var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!;
|
|
|
|
if (_connectionDto.ServerVersion != ILightlessHub.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}. " +
|
|
$"This client version is incompatible and will not be able to connect. Please update your Lightless Sync client.",
|
|
NotificationType.Error));
|
|
}
|
|
await StopConnectionAsync(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}. " +
|
|
$"Please keep your Lightless Sync client up-to-date.",
|
|
NotificationType.Warning));
|
|
}
|
|
|
|
if (_dalamudUtil.HasModifiedGameFiles)
|
|
{
|
|
Logger.LogError("Detected modified game files on connection");
|
|
if (!_lightlessConfigService.Current.DebugStopWhining)
|
|
Mediator.Publish(new NotificationMessage("Modified Game Files detected",
|
|
"Dalamud is reporting your FFXIV installation has modified game files. Any mods installed through TexTools will produce this message. " +
|
|
"Lightless Sync, Penumbra, and some other plugins assume your FFXIV installation is unmodified in order to work. " +
|
|
"Synchronization with pairs/shells can break because of this. Exit the game, open XIVLauncher, click the arrow next to Log In " +
|
|
"and select 'repair game files' to resolve this issue. Afterwards, do not install any mods with TexTools. Your plugin configurations will remain, as will mods enabled in Penumbra.",
|
|
NotificationType.Error, TimeSpan.FromSeconds(15)));
|
|
}
|
|
|
|
if (_dalamudUtil.IsLodEnabled && !_naggedAboutLod)
|
|
{
|
|
_naggedAboutLod = true;
|
|
Logger.LogWarning("Model LOD is enabled during connection");
|
|
if (!_lightlessConfigService.Current.DebugStopWhining)
|
|
{
|
|
Mediator.Publish(new NotificationMessage("Model LOD is enabled",
|
|
"You have \"Use low-detail models on distant objects (LOD)\" enabled. Having model LOD enabled is known to be a reason to cause " +
|
|
"random crashes when loading in or rendering modded pairs. Disabling LOD has a very low performance impact. Disable LOD while using Lightless: " +
|
|
"Go to XIV Menu -> System Configuration -> Graphics Settings and disable the model LOD option.", NotificationType.Warning, TimeSpan.FromSeconds(15)));
|
|
}
|
|
}
|
|
|
|
if (_naggedAboutLod && !_dalamudUtil.IsLodEnabled)
|
|
{
|
|
_naggedAboutLod = false;
|
|
}
|
|
|
|
await LoadIninitialPairsAsync().ConfigureAwait(false);
|
|
await LoadOnlinePairsAsync().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 StopConnectionAsync(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 StopConnectionAsync(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool _naggedAboutLod = false;
|
|
|
|
public Task CyclePauseAsync(Pair pair)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(pair);
|
|
return CyclePauseAsync(pair.UniqueIdent);
|
|
}
|
|
|
|
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
|
{
|
|
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
|
_ = Task.Run(async () =>
|
|
{
|
|
var token = timeoutCts.Token;
|
|
try
|
|
{
|
|
if (!_pairCoordinator.Ledger.TryGetEntry(ident, out var entry) || entry is null)
|
|
{
|
|
Logger.LogWarning("CyclePauseAsync: pair {uid} not found in ledger", ident.UserId);
|
|
return;
|
|
}
|
|
|
|
var originalPermissions = entry.SelfPermissions;
|
|
var targetPermissions = originalPermissions;
|
|
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
|
|
|
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
|
|
|
var applied = false;
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
|
{
|
|
if (updated.SelfPermissions == targetPermissions)
|
|
{
|
|
applied = true;
|
|
entry = updated;
|
|
break;
|
|
}
|
|
}
|
|
|
|
await Task.Delay(250, token).ConfigureAwait(false);
|
|
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
|
}
|
|
|
|
if (!applied)
|
|
{
|
|
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
|
return;
|
|
}
|
|
|
|
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Logger.LogDebug("CyclePauseAsync cancelled for {uid}", ident.UserId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "CyclePauseAsync failed for {uid}", ident.UserId);
|
|
}
|
|
finally
|
|
{
|
|
timeoutCts.Dispose();
|
|
}
|
|
}, CancellationToken.None);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task PauseAsync(UserData userData)
|
|
{
|
|
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
|
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
|
{
|
|
Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID);
|
|
return;
|
|
}
|
|
|
|
var permissions = entry.SelfPermissions;
|
|
permissions.SetPaused(paused: true);
|
|
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
|
}
|
|
|
|
public Task<ConnectionDto> GetConnectionDto() => GetConnectionDtoAsync(true);
|
|
|
|
public async Task<ConnectionDto> GetConnectionDtoAsync(bool publishConnected)
|
|
{
|
|
var dto = await _lightlessHub!.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 StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
|
|
_connectionCancellationTokenSource?.Cancel();
|
|
}
|
|
|
|
private async Task ClientHealthCheckAsync(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
if (_lightlessHub is null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
|
|
Logger.LogDebug("Checking Client Health State");
|
|
|
|
bool requireReconnect = await RefreshTokenAsync(ct).ConfigureAwait(false);
|
|
|
|
if (requireReconnect) break;
|
|
|
|
_ = await CheckClientHealth().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private void DalamudUtilOnLogIn()
|
|
{
|
|
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
|
|
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
|
var auth = _serverManager.CurrentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
|
|
if (auth?.AutoLogin ?? false)
|
|
{
|
|
Logger.LogInformation("Logging into {chara}", charaName);
|
|
_ = Task.Run(CreateConnectionsAsync);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Not logging into {chara}, auto login disabled", charaName);
|
|
_ = Task.Run(async () => await StopConnectionAsync(ServerState.NoAutoLogon).ConfigureAwait(false));
|
|
}
|
|
}
|
|
|
|
private void DalamudUtilOnLogOut()
|
|
{
|
|
_ = Task.Run(async () => await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
|
|
ServerState = ServerState.Offline;
|
|
}
|
|
|
|
private void InitializeApiHooks()
|
|
{
|
|
if (_lightlessHub == null) return;
|
|
|
|
Logger.LogDebug("Initializing data");
|
|
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
|
|
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
|
|
OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto));
|
|
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));
|
|
OnUserDefaultPermissionUpdate(dto => _ = Client_UserUpdateDefaultPermissions(dto));
|
|
OnUpdateUserIndividualPairStatusDto(dto => _ = Client_UpdateUserIndividualPairStatusDto(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));
|
|
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
|
|
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
|
if (!_initialized)
|
|
{
|
|
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
|
|
}
|
|
|
|
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();
|
|
_ = ClientHealthCheckAsync(_healthCheckTokenSource.Token);
|
|
|
|
_initialized = true;
|
|
}
|
|
|
|
private readonly HashSet<Action<ChatMessageDto>> _chatHandlers = new();
|
|
|
|
public void RegisterChatMessageHandler(Action<ChatMessageDto> handler)
|
|
{
|
|
if (_chatHandlers.Add(handler))
|
|
{
|
|
ChatMessageReceived += handler;
|
|
}
|
|
}
|
|
|
|
public void UnregisterChatMessageHandler(Action<ChatMessageDto> handler)
|
|
{
|
|
if (_chatHandlers.Remove(handler))
|
|
{
|
|
ChatMessageReceived -= handler;
|
|
}
|
|
}
|
|
|
|
private async Task LoadIninitialPairsAsync()
|
|
{
|
|
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
|
|
{
|
|
Logger.LogDebug("Group: {entry}", entry);
|
|
_pairCoordinator.HandleGroupFullInfo(entry);
|
|
}
|
|
|
|
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
|
{
|
|
Logger.LogDebug("Individual Pair: {userPair}", userPair);
|
|
_pairCoordinator.HandleUserAddPair(userPair);
|
|
}
|
|
}
|
|
|
|
private async Task LoadOnlinePairsAsync()
|
|
{
|
|
CensusDataDto? dto = null;
|
|
if (_serverManager.SendCensusData && _lastCensus != null)
|
|
{
|
|
var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false);
|
|
dto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
|
|
Logger.LogDebug("Attaching Census Data: {data}", dto);
|
|
}
|
|
|
|
foreach (var entry in await UserGetOnlinePairs(dto).ConfigureAwait(false))
|
|
{
|
|
Logger.LogDebug("Pair online: {pair}", entry);
|
|
_pairCoordinator.HandleUserOnline(entry, sendNotification: false);
|
|
}
|
|
}
|
|
|
|
private void LightlessHubOnClosed(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 LightlessHubOnReconnectedAsync()
|
|
{
|
|
ServerState = ServerState.Reconnecting;
|
|
try
|
|
{
|
|
InitializeApiHooks();
|
|
_connectionDto = await GetConnectionDtoAsync(publishConnected: false).ConfigureAwait(false);
|
|
if (_connectionDto.ServerVersion != ILightlessHub.ApiVersion)
|
|
{
|
|
await StopConnectionAsync(ServerState.VersionMisMatch).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
ServerState = ServerState.Connected;
|
|
OnConnected?.Invoke();
|
|
await LoadIninitialPairsAsync().ConfigureAwait(false);
|
|
await LoadOnlinePairsAsync().ConfigureAwait(false);
|
|
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogCritical(ex, "Failure to obtain data after reconnection");
|
|
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private void LightlessHubOnReconnecting(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<bool> RefreshTokenAsync(CancellationToken ct)
|
|
{
|
|
bool requireReconnect = false;
|
|
try
|
|
{
|
|
var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
|
if (!string.Equals(token, _lastUsedToken, StringComparison.Ordinal))
|
|
{
|
|
Logger.LogDebug("Reconnecting due to updated token");
|
|
|
|
_doNotNotifyOnNextInfo = true;
|
|
await CreateConnectionsAsync().ConfigureAwait(false);
|
|
requireReconnect = true;
|
|
}
|
|
}
|
|
catch (LightlessAuthFailureException ex)
|
|
{
|
|
AuthFailureMessage = ex.Reason;
|
|
await StopConnectionAsync(ServerState.Unauthorized).ConfigureAwait(false);
|
|
requireReconnect = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "Could not refresh token, forcing reconnect");
|
|
_doNotNotifyOnNextInfo = true;
|
|
await CreateConnectionsAsync().ConfigureAwait(false);
|
|
requireReconnect = true;
|
|
}
|
|
|
|
return requireReconnect;
|
|
}
|
|
|
|
private async Task StopConnectionAsync(ServerState state)
|
|
{
|
|
ServerState = ServerState.Disconnecting;
|
|
|
|
Logger.LogInformation("Stopping existing connection");
|
|
await _hubFactory.DisposeHubAsync().ConfigureAwait(false);
|
|
|
|
if (_lightlessHub 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;
|
|
if (_healthCheckTokenSource != null)
|
|
{
|
|
await _healthCheckTokenSource.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
Mediator.Publish(new DisconnectedMessage());
|
|
_lightlessHub = null;
|
|
_connectionDto = null;
|
|
_zoneChatChannels = Array.Empty<ZoneChatChannelInfoDto>();
|
|
_groupChatChannels = Array.Empty<GroupChatChannelInfoDto>();
|
|
}
|
|
|
|
ServerState = state;
|
|
}
|
|
}
|
|
#pragma warning restore MA0040
|