rebuild project & update internal names
This commit is contained in:
137
LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs
Normal file
137
LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.WebAPI;
|
||||
|
||||
#pragma warning disable MA0040
|
||||
public partial class ApiController
|
||||
{
|
||||
public async Task PushCharacterData(CharacterData data, List<UserData> visibleCharacters)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
await PushCharacterDataInternal(data, [.. visibleCharacters]).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogDebug("Upload operation was cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error during upload of files");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UserAddPair(UserDto user)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _mareHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserDelete()
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false);
|
||||
await CreateConnectionsAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs(CensusDataDto? censusDataDto)
|
||||
{
|
||||
return await _mareHub!.InvokeAsync<List<OnlineUserIdentDto>>(nameof(UserGetOnlinePairs), censusDataDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<UserFullPairDto>> UserGetPairedClients()
|
||||
{
|
||||
return await _mareHub!.InvokeAsync<List<UserFullPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
||||
{
|
||||
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null);
|
||||
return await _mareHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserPushData(UserCharaDataMessageDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to Push character data");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetBulkPermissions(BulkPermissionsDto dto)
|
||||
{
|
||||
CheckConnection();
|
||||
|
||||
try
|
||||
{
|
||||
await _mareHub!.InvokeAsync(nameof(SetBulkPermissions), dto).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to set permissions");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UserRemovePair(UserDto userDto)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _mareHub!.SendAsync(nameof(UserRemovePair), userDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserSetPairPermissions(UserPermissionsDto userPermissions)
|
||||
{
|
||||
await SetBulkPermissions(new(new(StringComparer.Ordinal)
|
||||
{
|
||||
{ userPermissions.User.UID, userPermissions.Permissions }
|
||||
}, new(StringComparer.Ordinal))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserSetProfile(UserProfileDto userDescription)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.InvokeAsync(nameof(UserUpdateDefaultPermissions), defaultPermissionsDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
|
||||
{
|
||||
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
||||
StringBuilder sb = new();
|
||||
foreach (var kvp in character.FileReplacements.ToList())
|
||||
{
|
||||
sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}");
|
||||
}
|
||||
foreach (var item in character.GlamourerData)
|
||||
{
|
||||
sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}");
|
||||
}
|
||||
Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString());
|
||||
|
||||
CensusDataDto? censusDto = null;
|
||||
if (_serverManager.SendCensusData && _lastCensus != null)
|
||||
{
|
||||
var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false);
|
||||
censusDto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
|
||||
Logger.LogDebug("Attaching Census Data: {data}", censusDto);
|
||||
}
|
||||
|
||||
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
||||
@@ -0,0 +1,400 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.MareConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.UI.MapMarkerData.Delegates;
|
||||
|
||||
namespace LightlessSync.WebAPI;
|
||||
|
||||
public partial class ApiController
|
||||
{
|
||||
public Task Client_DownloadReady(Guid requestId)
|
||||
{
|
||||
Logger.LogDebug("Server sent {requestId} ready", requestId);
|
||||
Mediator.Publish(new DownloadReadyMessage(requestId));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupChangePermissions: {perm}", groupPermission);
|
||||
ExecuteSafely(() => _pairManager.SetGroupPermissions(groupPermission));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_GroupChangeUserPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateGroupPairPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupDelete(GroupDto groupDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupDelete: {dto}", groupDto);
|
||||
ExecuteSafely(() => _pairManager.RemoveGroup(groupDto.Group));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo);
|
||||
ExecuteSafely(() =>
|
||||
{
|
||||
if (string.Equals(userInfo.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(userInfo);
|
||||
else _pairManager.SetGroupPairStatusInfo(userInfo);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairJoined: {dto}", groupPairInfoDto);
|
||||
ExecuteSafely(() => _pairManager.AddGroupPair(groupPairInfoDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairLeft(GroupPairDto groupPairDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairLeft: {dto}", groupPairDto);
|
||||
ExecuteSafely(() => _pairManager.RemoveGroupPair(groupPairDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupSendFullInfo: {dto}", groupInfo);
|
||||
ExecuteSafely(() => _pairManager.AddGroup(groupInfo));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupSendInfo(GroupInfoDto groupInfo)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupSendInfo: {dto}", groupInfo);
|
||||
ExecuteSafely(() => _pairManager.SetGroupInfo(groupInfo));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message)
|
||||
{
|
||||
switch (messageSeverity)
|
||||
{
|
||||
case MessageSeverity.Error:
|
||||
Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Error, TimeSpan.FromSeconds(7.5)));
|
||||
break;
|
||||
|
||||
case MessageSeverity.Warning:
|
||||
Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Warning, TimeSpan.FromSeconds(7.5)));
|
||||
break;
|
||||
|
||||
case MessageSeverity.Information:
|
||||
if (_doNotNotifyOnNextInfo)
|
||||
{
|
||||
_doNotNotifyOnNextInfo = false;
|
||||
break;
|
||||
}
|
||||
Mediator.Publish(new NotificationMessage("Info from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
|
||||
{
|
||||
SystemInfoDto = systemInfo;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UpdateUserIndividualPairStatusDto: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateIndividualPairStatus(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserAddClientPair(UserPairDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserAddClientPair: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
|
||||
{
|
||||
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
|
||||
ExecuteSafely(() => _pairManager.ReceiveCharaData(dataDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserReceiveUploadStatus(UserDto dto)
|
||||
{
|
||||
Logger.LogTrace("Client_UserReceiveUploadStatus: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.ReceiveUploadStatus(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserRemoveClientPair(UserDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserRemoveClientPair: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.RemoveUserPair(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserSendOffline(UserDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserSendOffline: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserSendOnline(OnlineUserIdentDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserSendOnline: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.MarkPairOnline(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserUpdateDefaultPermissions(DefaultPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateDefaultPermissions: {dto}", dto);
|
||||
_connectionDto!.DefaultPreferredPermissions = dto;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserUpdateProfile(UserDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateProfile: {dto}", dto);
|
||||
ExecuteSafely(() => Mediator.Publish(new ClearProfileDataMessage(dto.User)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateSelfPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GposeLobbyJoin(UserData userData)
|
||||
{
|
||||
Logger.LogDebug("Client_GposeLobbyJoin: {dto}", userData);
|
||||
ExecuteSafely(() => Mediator.Publish(new GposeLobbyUserJoin(userData)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GposeLobbyLeave(UserData userData)
|
||||
{
|
||||
Logger.LogDebug("Client_GposeLobbyLeave: {dto}", userData);
|
||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyUserLeave(userData)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto)
|
||||
{
|
||||
Logger.LogDebug("Client_GposeLobbyPushCharacterData: {dto}", charaDownloadDto.Uploader);
|
||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveCharaData(charaDownloadDto)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData)
|
||||
{
|
||||
Logger.LogDebug("Client_GposeLobbyPushPoseData: {dto}", userData);
|
||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceivePoseData(userData, poseData)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData)
|
||||
{
|
||||
//Logger.LogDebug("Client_GposeLobbyPushWorldData: {dto}", userData);
|
||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void OnDownloadReady(Action<Guid> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_DownloadReady), act);
|
||||
}
|
||||
|
||||
public void OnGroupChangePermissions(Action<GroupPermissionDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupChangePermissions), act);
|
||||
}
|
||||
|
||||
public void OnGroupChangeUserPairPermissions(Action<GroupPairUserPermissionDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupChangeUserPairPermissions), act);
|
||||
}
|
||||
|
||||
public void OnGroupDelete(Action<GroupDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupDelete), act);
|
||||
}
|
||||
|
||||
public void OnGroupPairChangeUserInfo(Action<GroupPairUserInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupPairChangeUserInfo), act);
|
||||
}
|
||||
|
||||
public void OnGroupPairJoined(Action<GroupPairFullInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupPairJoined), act);
|
||||
}
|
||||
|
||||
public void OnGroupPairLeft(Action<GroupPairDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupPairLeft), act);
|
||||
}
|
||||
|
||||
public void OnGroupSendFullInfo(Action<GroupFullInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupSendFullInfo), act);
|
||||
}
|
||||
|
||||
public void OnGroupSendInfo(Action<GroupInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupSendInfo), act);
|
||||
}
|
||||
|
||||
public void OnReceiveServerMessage(Action<MessageSeverity, string> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_ReceiveServerMessage), act);
|
||||
}
|
||||
|
||||
public void OnUpdateSystemInfo(Action<SystemInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UpdateSystemInfo), act);
|
||||
}
|
||||
|
||||
public void OnUpdateUserIndividualPairStatusDto(Action<UserIndividualPairStatusDto> action)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UpdateUserIndividualPairStatusDto), action);
|
||||
}
|
||||
|
||||
public void OnUserAddClientPair(Action<UserPairDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserAddClientPair), act);
|
||||
}
|
||||
|
||||
public void OnUserDefaultPermissionUpdate(Action<DefaultPermissionsDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserUpdateDefaultPermissions), act);
|
||||
}
|
||||
|
||||
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserReceiveCharacterData), act);
|
||||
}
|
||||
|
||||
public void OnUserReceiveUploadStatus(Action<UserDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserReceiveUploadStatus), act);
|
||||
}
|
||||
|
||||
public void OnUserRemoveClientPair(Action<UserDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserRemoveClientPair), act);
|
||||
}
|
||||
|
||||
public void OnUserSendOffline(Action<UserDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserSendOffline), act);
|
||||
}
|
||||
|
||||
public void OnUserSendOnline(Action<OnlineUserIdentDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserSendOnline), act);
|
||||
}
|
||||
|
||||
public void OnUserUpdateOtherPairPermissions(Action<UserPermissionsDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserUpdateOtherPairPermissions), act);
|
||||
}
|
||||
|
||||
public void OnUserUpdateProfile(Action<UserDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserUpdateProfile), act);
|
||||
}
|
||||
|
||||
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act);
|
||||
}
|
||||
|
||||
public void OnGposeLobbyJoin(Action<UserData> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GposeLobbyJoin), act);
|
||||
}
|
||||
|
||||
public void OnGposeLobbyLeave(Action<UserData> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GposeLobbyLeave), act);
|
||||
}
|
||||
|
||||
public void OnGposeLobbyPushCharacterData(Action<CharaDataDownloadDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GposeLobbyPushCharacterData), act);
|
||||
}
|
||||
|
||||
public void OnGposeLobbyPushPoseData(Action<UserData, PoseData> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GposeLobbyPushPoseData), act);
|
||||
}
|
||||
|
||||
public void OnGposeLobbyPushWorldData(Action<UserData, WorldData> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
|
||||
}
|
||||
|
||||
private void ExecuteSafely(Action act)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogCritical(ex, "Error on executing safely");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.Services.CharaData.Models;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.WebAPI;
|
||||
public partial class ApiController
|
||||
{
|
||||
public async Task<CharaDataFullDto?> CharaDataCreate()
|
||||
{
|
||||
if (!IsConnected) return null;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Creating new Character Data");
|
||||
return await _mareHub!.InvokeAsync<CharaDataFullDto?>(nameof(CharaDataCreate)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to create new character data");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto)
|
||||
{
|
||||
if (!IsConnected) return null;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Updating chara data for {id}", updateDto.Id);
|
||||
return await _mareHub!.InvokeAsync<CharaDataFullDto?>(nameof(CharaDataUpdate), updateDto).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to update chara data for {id}", updateDto.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CharaDataDelete(string id)
|
||||
{
|
||||
if (!IsConnected) return false;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Deleting chara data for {id}", id);
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(CharaDataDelete), id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to delete chara data for {id}", id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id)
|
||||
{
|
||||
if (!IsConnected) return null;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Getting metainfo for chara data {id}", id);
|
||||
return await _mareHub!.InvokeAsync<CharaDataMetaInfoDto?>(nameof(CharaDataGetMetainfo), id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to get meta info for chara data {id}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CharaDataFullDto?> CharaDataAttemptRestore(string id)
|
||||
{
|
||||
if (!IsConnected) return null;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Attempting to restore chara data {id}", id);
|
||||
return await _mareHub!.InvokeAsync<CharaDataFullDto?>(nameof(CharaDataAttemptRestore), id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to restore chara data for {id}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CharaDataFullDto>> CharaDataGetOwn()
|
||||
{
|
||||
if (!IsConnected) return [];
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Getting all own chara data");
|
||||
return await _mareHub!.InvokeAsync<List<CharaDataFullDto>>(nameof(CharaDataGetOwn)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to get own chara data");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CharaDataMetaInfoDto>> CharaDataGetShared()
|
||||
{
|
||||
if (!IsConnected) return [];
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Getting all own chara data");
|
||||
return await _mareHub!.InvokeAsync<List<CharaDataMetaInfoDto>>(nameof(CharaDataGetShared)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to get shared chara data");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CharaDataDownloadDto?> CharaDataDownload(string id)
|
||||
{
|
||||
if (!IsConnected) return null;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Getting download chara data for {id}", id);
|
||||
return await _mareHub!.InvokeAsync<CharaDataDownloadDto>(nameof(CharaDataDownload), id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to get download chara data for {id}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GposeLobbyCreate()
|
||||
{
|
||||
if (!IsConnected) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Creating GPose Lobby");
|
||||
return await _mareHub!.InvokeAsync<string>(nameof(GposeLobbyCreate)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to create GPose lobby");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> GposeLobbyLeave()
|
||||
{
|
||||
if (!IsConnected) return true;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Leaving current GPose Lobby");
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(GposeLobbyLeave)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to leave GPose lobby");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<UserData>> GposeLobbyJoin(string lobbyId)
|
||||
{
|
||||
if (!IsConnected) return [];
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Joining GPose Lobby {id}", lobbyId);
|
||||
return await _mareHub!.InvokeAsync<List<UserData>>(nameof(GposeLobbyJoin), lobbyId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to join GPose lobby {id}", lobbyId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Sending Chara Data to GPose Lobby");
|
||||
await _mareHub!.InvokeAsync(nameof(GposeLobbyPushCharacterData), charaDownloadDto).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to send Chara Data to GPose lobby");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GposeLobbyPushPoseData(PoseData poseData)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Sending Pose Data to GPose Lobby");
|
||||
await _mareHub!.InvokeAsync(nameof(GposeLobbyPushPoseData), poseData).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to send Pose Data to GPose lobby");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GposeLobbyPushWorldData(WorldData worldData)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
await _mareHub!.InvokeAsync(nameof(GposeLobbyPushWorldData), worldData).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to send World Data to GPose lobby");
|
||||
}
|
||||
}
|
||||
}
|
||||
124
LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs
Normal file
124
LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace LightlessSync.WebAPI;
|
||||
|
||||
public partial class ApiController
|
||||
{
|
||||
public async Task GroupBanUser(GroupPairDto dto, string reason)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupBanUser), dto, reason).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupChangeGroupPermissionState), dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
CheckConnection();
|
||||
await SetBulkPermissions(new(new(StringComparer.Ordinal),
|
||||
new(StringComparer.Ordinal) {
|
||||
{ dto.Group.GID, dto.GroupPairPermissions }
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupChangeOwnership(GroupPairDto groupPair)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupChangeOwnership), groupPair).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> GroupChangePassword(GroupPasswordDto groupPassword)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupClear(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<GroupJoinDto> GroupCreate()
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<GroupJoinDto>(nameof(GroupCreate)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<List<string>>(nameof(GroupCreateTempInvite), group, amount).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupDelete(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupDelete), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<BannedGroupUserDto>> GroupGetBannedUsers(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<List<BannedGroupUserDto>>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<GroupJoinInfoDto> GroupJoin(GroupPasswordDto passwordedGroup)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<GroupJoinInfoDto>(nameof(GroupJoin), passwordedGroup).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> GroupJoinFinalize(GroupJoinDto passwordedGroup)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(GroupJoinFinalize), passwordedGroup).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupLeave(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupLeave), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupRemoveUser(GroupPairDto groupPair)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupRemoveUser), groupPair).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupSetUserInfo(GroupPairUserInfoDto groupPair)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupSetUserInfo), groupPair).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> GroupPrune(GroupDto group, int days, bool execute)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<int>(nameof(GroupPrune), group, days, execute).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<List<GroupFullInfoDto>>(nameof(GroupsGetAll)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupUnbanUser(GroupPairDto groupPair)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void CheckConnection()
|
||||
{
|
||||
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
||||
}
|
||||
}
|
||||
596
LightlessSync/WebAPI/SignalR/ApiController.cs
Normal file
596
LightlessSync/WebAPI/SignalR/ApiController.cs
Normal file
@@ -0,0 +1,596 @@
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSync.MareConfiguration;
|
||||
using LightlessSync.MareConfiguration.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;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LightlessSync.WebAPI;
|
||||
|
||||
#pragma warning disable MA0040
|
||||
public sealed partial class ApiController : DisposableMediatorSubscriberBase, ILightlessHubClient
|
||||
{
|
||||
public const string MainServer = "Lunae Crescere Incipientis (Official Central Server)";
|
||||
public const string MainServiceUri = "wss://LightlessSync.com";
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly HubFactory _hubFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private CancellationTokenSource _connectionCancellationTokenSource;
|
||||
private ConnectionDto? _connectionDto;
|
||||
private bool _doNotNotifyOnNextInfo = false;
|
||||
private CancellationTokenSource? _healthCheckTokenSource = new();
|
||||
private bool _initialized;
|
||||
private string? _lastUsedToken;
|
||||
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, MareConfigService mareConfigService) : base(logger, mediator)
|
||||
{
|
||||
_hubFactory = hubFactory;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_serverManager = serverManager;
|
||||
_tokenProvider = tokenProvider;
|
||||
_mareConfigService = mareConfigService;
|
||||
_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) => _ = MareHubOnReconnectedAsync());
|
||||
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => MareHubOnReconnecting(msg.Exception));
|
||||
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePauseAsync(msg.UserData));
|
||||
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 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 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);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
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 Mare.",
|
||||
NotificationType.Error));
|
||||
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (secretKey.IsNullOrEmpty())
|
||||
{
|
||||
Logger.LogWarning("No secret key set for current character");
|
||||
_connectionDto = null;
|
||||
await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
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 Mare.",
|
||||
NotificationType.Error));
|
||||
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oauth2.HasValue)
|
||||
{
|
||||
Logger.LogWarning("No UID/OAuth set for current character");
|
||||
_connectionDto = null;
|
||||
await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
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);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
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}")));
|
||||
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
_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;
|
||||
|
||||
_mareHub = _hubFactory.GetOrCreate(token);
|
||||
InitializeApiHooks();
|
||||
|
||||
await _mareHub.StartAsync(token).ConfigureAwait(false);
|
||||
|
||||
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||
|
||||
ServerState = ServerState.Connected;
|
||||
|
||||
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 Mare Synchronos 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 Mare Synchronos client up-to-date.",
|
||||
NotificationType.Warning));
|
||||
}
|
||||
|
||||
if (_dalamudUtil.HasModifiedGameFiles)
|
||||
{
|
||||
Logger.LogError("Detected modified game files on connection");
|
||||
if (!_mareConfigService.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. " +
|
||||
"Mare Synchronos, 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 (!_mareConfigService.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 Mare: " +
|
||||
"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(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 PauseAsync(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() => GetConnectionDtoAsync(true);
|
||||
|
||||
public async Task<ConnectionDto> GetConnectionDtoAsync(bool publishConnected)
|
||||
{
|
||||
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 StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
private async Task ClientHealthCheckAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested && _mareHub != null)
|
||||
{
|
||||
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 (_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));
|
||||
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));
|
||||
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(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();
|
||||
_ = ClientHealthCheckAsync(_healthCheckTokenSource.Token);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private async Task LoadIninitialPairsAsync()
|
||||
{
|
||||
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Group: {entry}", entry);
|
||||
_pairManager.AddGroup(entry);
|
||||
}
|
||||
|
||||
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Individual Pair: {userPair}", userPair);
|
||||
_pairManager.AddUserPair(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);
|
||||
_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 MareHubOnReconnectedAsync()
|
||||
{
|
||||
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;
|
||||
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 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<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 (_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
|
||||
141
LightlessSync/WebAPI/SignalR/HubFactory.cs
Normal file
141
LightlessSync/WebAPI/SignalR/HubFactory.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.WebAPI.SignalR;
|
||||
|
||||
public class HubFactory : MediatorSubscriberBase
|
||||
{
|
||||
private readonly ILoggerProvider _loggingProvider;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private HubConnection? _instance;
|
||||
private bool _isDisposed = false;
|
||||
private readonly bool _isWine = false;
|
||||
|
||||
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
TokenProvider tokenProvider, ILoggerProvider pluginLog,
|
||||
DalamudUtilService dalamudUtilService) : base(logger, mediator)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_tokenProvider = tokenProvider;
|
||||
_loggingProvider = pluginLog;
|
||||
_isWine = dalamudUtilService.IsWine;
|
||||
}
|
||||
|
||||
public async Task DisposeHubAsync()
|
||||
{
|
||||
if (_instance == null || _isDisposed) return;
|
||||
|
||||
Logger.LogDebug("Disposing current HubConnection");
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_instance.Closed -= HubOnClosed;
|
||||
_instance.Reconnecting -= HubOnReconnecting;
|
||||
_instance.Reconnected -= HubOnReconnected;
|
||||
|
||||
await _instance.StopAsync().ConfigureAwait(false);
|
||||
await _instance.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
_instance = null;
|
||||
|
||||
Logger.LogDebug("Current HubConnection disposed");
|
||||
}
|
||||
|
||||
public HubConnection GetOrCreate(CancellationToken ct)
|
||||
{
|
||||
if (!_isDisposed && _instance != null) return _instance;
|
||||
|
||||
return BuildHubConnection(ct);
|
||||
}
|
||||
|
||||
private HubConnection BuildHubConnection(CancellationToken ct)
|
||||
{
|
||||
var transportType = _serverConfigurationManager.GetTransport() switch
|
||||
{
|
||||
HttpTransportType.None => HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling,
|
||||
HttpTransportType.WebSockets => HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling,
|
||||
HttpTransportType.ServerSentEvents => HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling,
|
||||
HttpTransportType.LongPolling => HttpTransportType.LongPolling,
|
||||
_ => HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling
|
||||
};
|
||||
|
||||
if (_isWine && !_serverConfigurationManager.CurrentServer.ForceWebSockets
|
||||
&& transportType.HasFlag(HttpTransportType.WebSockets))
|
||||
{
|
||||
Logger.LogDebug("Wine detected, falling back to ServerSentEvents / LongPolling");
|
||||
transportType = HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Building new HubConnection using transport {transport}", transportType);
|
||||
|
||||
_instance = new HubConnectionBuilder()
|
||||
.WithUrl(_serverConfigurationManager.CurrentApiUrl + ILightlessHub.Path, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => _tokenProvider.GetOrUpdateToken(ct);
|
||||
options.Transports = transportType;
|
||||
})
|
||||
.AddMessagePackProtocol(opt =>
|
||||
{
|
||||
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
|
||||
BuiltinResolver.Instance,
|
||||
AttributeFormatterResolver.Instance,
|
||||
// replace enum resolver
|
||||
DynamicEnumAsStringResolver.Instance,
|
||||
DynamicGenericResolver.Instance,
|
||||
DynamicUnionResolver.Instance,
|
||||
DynamicObjectResolver.Instance,
|
||||
PrimitiveObjectResolver.Instance,
|
||||
// final fallback(last priority)
|
||||
StandardResolver.Instance);
|
||||
|
||||
opt.SerializerOptions =
|
||||
MessagePackSerializerOptions.Standard
|
||||
.WithCompression(MessagePackCompression.Lz4Block)
|
||||
.WithResolver(resolver);
|
||||
})
|
||||
.WithAutomaticReconnect(new ForeverRetryPolicy(Mediator))
|
||||
.ConfigureLogging(a =>
|
||||
{
|
||||
a.ClearProviders().AddProvider(_loggingProvider);
|
||||
a.SetMinimumLevel(LogLevel.Information);
|
||||
})
|
||||
.Build();
|
||||
|
||||
_instance.Closed += HubOnClosed;
|
||||
_instance.Reconnecting += HubOnReconnecting;
|
||||
_instance.Reconnected += HubOnReconnected;
|
||||
|
||||
_isDisposed = false;
|
||||
|
||||
return _instance;
|
||||
}
|
||||
|
||||
private Task HubOnClosed(Exception? arg)
|
||||
{
|
||||
Mediator.Publish(new HubClosedMessage(arg));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task HubOnReconnected(string? arg)
|
||||
{
|
||||
Mediator.Publish(new HubReconnectedMessage(arg));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task HubOnReconnecting(Exception? arg)
|
||||
{
|
||||
Mediator.Publish(new HubReconnectingMessage(arg));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
9
LightlessSync/WebAPI/SignalR/JwtIdentifier.cs
Normal file
9
LightlessSync/WebAPI/SignalR/JwtIdentifier.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace LightlessSync.WebAPI.SignalR;
|
||||
|
||||
public record JwtIdentifier(string ApiUrl, string CharaHash, string UID, string SecretKeyOrOAuth)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return "{JwtIdentifier; Url: " + ApiUrl + ", Chara: " + CharaHash + ", UID: " + UID + ", HasSecretKeyOrOAuth: " + !string.IsNullOrEmpty(SecretKeyOrOAuth) + "}";
|
||||
}
|
||||
}
|
||||
11
LightlessSync/WebAPI/SignalR/MareAuthFailureException.cs
Normal file
11
LightlessSync/WebAPI/SignalR/MareAuthFailureException.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace LightlessSync.WebAPI.SignalR;
|
||||
|
||||
public class LightlessAuthFailureException : Exception
|
||||
{
|
||||
public LightlessAuthFailureException(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public string Reason { get; }
|
||||
}
|
||||
278
LightlessSync/WebAPI/SignalR/TokenProvider.cs
Normal file
278
LightlessSync/WebAPI/SignalR/TokenProvider.cs
Normal file
@@ -0,0 +1,278 @@
|
||||
using LightlessSync.API.Routes;
|
||||
using LightlessSync.MareConfiguration.Models;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LightlessSync.WebAPI.SignalR;
|
||||
|
||||
public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<TokenProvider> _logger;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new();
|
||||
|
||||
public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil, MareMediator mareMediator, HttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_serverManager = serverManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
Mediator = mareMediator;
|
||||
_httpClient = httpClient;
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) =>
|
||||
{
|
||||
_lastJwtIdentifier = null;
|
||||
_tokenCache.Clear();
|
||||
});
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) =>
|
||||
{
|
||||
_lastJwtIdentifier = null;
|
||||
_tokenCache.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
public MareMediator Mediator { get; }
|
||||
|
||||
private JwtIdentifier? _lastJwtIdentifier;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Mediator.UnsubscribeAll(this);
|
||||
}
|
||||
|
||||
public async Task<string> GetNewToken(bool isRenewal, JwtIdentifier identifier, CancellationToken ct)
|
||||
{
|
||||
Uri tokenUri;
|
||||
string response = string.Empty;
|
||||
HttpResponseMessage result;
|
||||
|
||||
try
|
||||
{
|
||||
if (!isRenewal)
|
||||
{
|
||||
_logger.LogDebug("GetNewToken: Requesting");
|
||||
|
||||
if (!_serverManager.CurrentServer.UseOAuth2)
|
||||
{
|
||||
tokenUri = LightlessAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
var secretKey = _serverManager.GetSecretKey(out _)!;
|
||||
var auth = secretKey.GetHash256();
|
||||
_logger.LogInformation("Sending SecretKey Request to server with auth {auth}", string.Join("", identifier.SecretKeyOrOAuth.Take(10)));
|
||||
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(
|
||||
[
|
||||
new KeyValuePair<string, string>("auth", auth),
|
||||
new KeyValuePair<string, string>("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
|
||||
]), ct).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
tokenUri = LightlessAuth.AuthWithOauthFullPath(new Uri(_serverManager.CurrentApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, tokenUri.ToString());
|
||||
request.Content = new FormUrlEncodedContent([
|
||||
new KeyValuePair<string, string>("uid", identifier.UID),
|
||||
new KeyValuePair<string, string>("charaIdent", identifier.CharaHash)
|
||||
]);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", identifier.SecretKeyOrOAuth);
|
||||
_logger.LogInformation("Sending OAuth Request to server with auth {auth}", string.Join("", identifier.SecretKeyOrOAuth.Take(10)));
|
||||
result = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("GetNewToken: Renewal");
|
||||
|
||||
tokenUri = LightlessAuth.RenewTokenFullPath(new Uri(_serverManager.CurrentApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
HttpRequestMessage request = new(HttpMethod.Get, tokenUri.ToString());
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenCache[identifier]);
|
||||
result = await _httpClient.SendAsync(request, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
response = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
result.EnsureSuccessStatusCode();
|
||||
_tokenCache[identifier] = response;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_tokenCache.TryRemove(identifier, out _);
|
||||
|
||||
_logger.LogError(ex, "GetNewToken: Failure to get token");
|
||||
|
||||
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (isRenewal)
|
||||
Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting to Mare manually.",
|
||||
NotificationType.Error));
|
||||
else
|
||||
Mediator.Publish(new NotificationMessage("Error generating token", "Your authentication token could not be generated. Check Mares Main UI (/mare in chat) to see the error message.",
|
||||
NotificationType.Error));
|
||||
Mediator.Publish(new DisconnectedMessage());
|
||||
throw new LightlessAuthFailureException(response);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = handler.ReadJwtToken(response);
|
||||
_logger.LogTrace("GetNewToken: JWT {token}", response);
|
||||
_logger.LogDebug("GetNewToken: Valid until {date}, ValidClaim until {date}", jwtToken.ValidTo,
|
||||
new DateTime(long.Parse(jwtToken.Claims.Single(c => string.Equals(c.Type, "expiration_date", StringComparison.Ordinal)).Value), DateTimeKind.Utc));
|
||||
var dateTimeMinus10 = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10));
|
||||
var dateTimePlus10 = DateTime.UtcNow.Add(TimeSpan.FromMinutes(10));
|
||||
var tokenTime = jwtToken.ValidTo.Subtract(TimeSpan.FromHours(6));
|
||||
if (tokenTime <= dateTimeMinus10 || tokenTime >= dateTimePlus10)
|
||||
{
|
||||
_tokenCache.TryRemove(identifier, out _);
|
||||
Mediator.Publish(new NotificationMessage("Invalid system clock", "The clock of your computer is invalid. " +
|
||||
"Mare will not function properly if the time zone is not set correctly. " +
|
||||
"Please set your computers time zone correctly and keep your clock synchronized with the internet.",
|
||||
NotificationType.Error));
|
||||
throw new InvalidOperationException($"JwtToken is behind DateTime.UtcNow, DateTime.UtcNow is possibly wrong. DateTime.UtcNow is {DateTime.UtcNow}, JwtToken.ValidTo is {jwtToken.ValidTo}");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private async Task<JwtIdentifier?> GetIdentifier()
|
||||
{
|
||||
JwtIdentifier jwtIdentifier;
|
||||
try
|
||||
{
|
||||
var playerIdentifier = await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(playerIdentifier))
|
||||
{
|
||||
_logger.LogTrace("GetIdentifier: PlayerIdentifier was null, returning last identifier {identifier}", _lastJwtIdentifier);
|
||||
return _lastJwtIdentifier;
|
||||
}
|
||||
|
||||
if (_serverManager.CurrentServer.UseOAuth2)
|
||||
{
|
||||
var (OAuthToken, UID) = _serverManager.GetOAuth2(out _)
|
||||
?? throw new InvalidOperationException("Requested OAuth2 but received null");
|
||||
|
||||
jwtIdentifier = new(_serverManager.CurrentApiUrl,
|
||||
playerIdentifier,
|
||||
UID, OAuthToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var secretKey = _serverManager.GetSecretKey(out _)
|
||||
?? throw new InvalidOperationException("Requested SecretKey but received null");
|
||||
|
||||
jwtIdentifier = new(_serverManager.CurrentApiUrl,
|
||||
playerIdentifier,
|
||||
string.Empty,
|
||||
secretKey);
|
||||
}
|
||||
_lastJwtIdentifier = jwtIdentifier;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_lastJwtIdentifier == null)
|
||||
{
|
||||
_logger.LogError("GetIdentifier: No last identifier found, aborting");
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogWarning(ex, "GetIdentifier: Could not get JwtIdentifier for some reason or another, reusing last identifier {identifier}", _lastJwtIdentifier);
|
||||
jwtIdentifier = _lastJwtIdentifier;
|
||||
}
|
||||
|
||||
_logger.LogDebug("GetIdentifier: Using identifier {identifier}", jwtIdentifier);
|
||||
return jwtIdentifier;
|
||||
}
|
||||
|
||||
public async Task<string?> GetToken()
|
||||
{
|
||||
JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false);
|
||||
if (jwtIdentifier == null) return null;
|
||||
|
||||
if (_tokenCache.TryGetValue(jwtIdentifier, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No token present");
|
||||
}
|
||||
|
||||
public async Task<string?> GetOrUpdateToken(CancellationToken ct)
|
||||
{
|
||||
JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false);
|
||||
if (jwtIdentifier == null) return null;
|
||||
|
||||
bool renewal = false;
|
||||
if (_tokenCache.TryGetValue(jwtIdentifier, out var token))
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
if (jwt.ValidTo == DateTime.MinValue || jwt.ValidTo.Subtract(TimeSpan.FromMinutes(5)) > DateTime.UtcNow)
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
_logger.LogDebug("GetOrUpdate: Cached token requires renewal, token valid to: {valid}, UtcTime is {utcTime}", jwt.ValidTo, DateTime.UtcNow);
|
||||
renewal = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("GetOrUpdate: Did not find token in cache, requesting a new one");
|
||||
}
|
||||
|
||||
_logger.LogTrace("GetOrUpdate: Getting new token");
|
||||
return await GetNewToken(renewal, jwtIdentifier, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> TryUpdateOAuth2LoginTokenAsync(ServerStorage currentServer, bool forced = false)
|
||||
{
|
||||
var oauth2 = _serverManager.GetOAuth2(out _);
|
||||
if (oauth2 == null) return false;
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(oauth2.Value.OAuthToken);
|
||||
if (!forced)
|
||||
{
|
||||
if (jwt.ValidTo == DateTime.MinValue || jwt.ValidTo.Subtract(TimeSpan.FromDays(7)) > DateTime.Now)
|
||||
return true;
|
||||
|
||||
if (jwt.ValidTo < DateTime.UtcNow)
|
||||
return false;
|
||||
}
|
||||
|
||||
var tokenUri = LightlessAuth.RenewOAuthTokenFullPath(new Uri(currentServer.ServerUri
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, tokenUri.ToString());
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", oauth2.Value.OAuthToken);
|
||||
_logger.LogInformation("Sending Request to server with auth {auth}", string.Join("", oauth2.Value.OAuthToken.Take(10)));
|
||||
var result = await _httpClient.SendAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Could not renew OAuth2 Login token, error code {error}", result.StatusCode);
|
||||
currentServer.OAuthToken = null;
|
||||
_serverManager.Save();
|
||||
return false;
|
||||
}
|
||||
|
||||
var newToken = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
currentServer.OAuthToken = newToken;
|
||||
_serverManager.Save();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
39
LightlessSync/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs
Normal file
39
LightlessSync/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using LightlessSync.MareConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace LightlessSync.WebAPI.SignalR.Utils;
|
||||
|
||||
public class ForeverRetryPolicy : IRetryPolicy
|
||||
{
|
||||
private readonly MareMediator _mediator;
|
||||
private bool _sentDisconnected = false;
|
||||
|
||||
public ForeverRetryPolicy(MareMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public TimeSpan? NextRetryDelay(RetryContext retryContext)
|
||||
{
|
||||
TimeSpan timeToWait = TimeSpan.FromSeconds(new Random().Next(10, 20));
|
||||
if (retryContext.PreviousRetryCount == 0)
|
||||
{
|
||||
_sentDisconnected = false;
|
||||
timeToWait = TimeSpan.FromSeconds(3);
|
||||
}
|
||||
else if (retryContext.PreviousRetryCount == 1) timeToWait = TimeSpan.FromSeconds(5);
|
||||
else if (retryContext.PreviousRetryCount == 2) timeToWait = TimeSpan.FromSeconds(10);
|
||||
else
|
||||
{
|
||||
if (!_sentDisconnected)
|
||||
{
|
||||
_mediator.Publish(new NotificationMessage("Connection lost", "Connection lost to server", NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||
_mediator.Publish(new DisconnectedMessage());
|
||||
}
|
||||
_sentDisconnected = true;
|
||||
}
|
||||
|
||||
return timeToWait;
|
||||
}
|
||||
}
|
||||
19
LightlessSync/WebAPI/SignalR/Utils/ServerState.cs
Normal file
19
LightlessSync/WebAPI/SignalR/Utils/ServerState.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace LightlessSync.WebAPI.SignalR.Utils;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
Offline,
|
||||
Connecting,
|
||||
Reconnecting,
|
||||
Disconnecting,
|
||||
Disconnected,
|
||||
Connected,
|
||||
Unauthorized,
|
||||
VersionMisMatch,
|
||||
RateLimited,
|
||||
NoSecretKey,
|
||||
MultiChara,
|
||||
OAuthMisconfigured,
|
||||
OAuthLoginTokenStale,
|
||||
NoAutoLogon
|
||||
}
|
||||
Reference in New Issue
Block a user