Initial
This commit is contained in:
116
MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs
Normal file
116
MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.WebAPI;
|
||||
|
||||
public partial class ApiController
|
||||
{
|
||||
public async Task PushCharacterData(CharacterData data, List<UserData> visibleCharacters)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Pushing Character data {hash} to {visible}", data.DataHash, string.Join(", ", visibleCharacters.Select(v => v.AliasOrUID)));
|
||||
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 UserChatSendMsg(UserDto user, ChatMessage message)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(UserChatSendMsg), user, message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserDelete()
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false);
|
||||
await CreateConnections().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs()
|
||||
{
|
||||
return await _mareHub!.InvokeAsync<List<OnlineUserIdentDto>>(nameof(UserGetOnlinePairs)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<UserPairDto>> UserGetPairedClients()
|
||||
{
|
||||
return await _mareHub!.InvokeAsync<List<UserPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
||||
{
|
||||
if (!IsConnected) return new UserProfileDto(dto.User, false, null, null, 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 UserRemovePair(UserDto userDto)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _mareHub!.SendAsync(nameof(UserRemovePair), userDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserReportProfile(UserProfileReportDto userDto)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _mareHub!.SendAsync(nameof(UserReportProfile), userDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserSetPairPermissions(UserPermissionsDto userPermissions)
|
||||
{
|
||||
await _mareHub!.SendAsync(nameof(UserSetPairPermissions), userPermissions).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserSetProfile(UserProfileDto userDescription)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).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());
|
||||
|
||||
await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,405 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.Dto.Chat;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.UI.MapMarkerData.Delegates;
|
||||
|
||||
namespace MareSynchronos.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_GroupChatMsg(GroupChatMsgDto groupChatMsgDto)
|
||||
{
|
||||
Logger.LogDebug("Client_GroupChatMsg: {msg}", groupChatMsgDto.Message);
|
||||
Mediator.Publish(new GroupChatMsgMessage(groupChatMsgDto.Group, groupChatMsgDto.Message));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairChangePermissions: {dto}", dto);
|
||||
ExecuteSafely(() =>
|
||||
{
|
||||
if (string.Equals(dto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(dto);
|
||||
else _pairManager.SetGroupPairUserPermissions(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_UserAddClientPair(UserPairDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserAddClientPair: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserChatMsg(UserChatMsgDto chatMsgDto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserChatMsg: {msg}", chatMsgDto.Message);
|
||||
Mediator.Publish(new UserChatMsgMessage(chatMsgDto.Message));
|
||||
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_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 OnGroupChatMsg(Action<GroupChatMsgDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupChatMsg), act);
|
||||
}
|
||||
|
||||
public void OnGroupPairChangePermissions(Action<GroupPairUserPermissionDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_GroupPairChangePermissions), 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 OnUserAddClientPair(Action<UserPairDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserAddClientPair), act);
|
||||
}
|
||||
|
||||
public void OnUserChatMsg(Action<UserChatMsgDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_mareHub!.On(nameof(Client_UserChatMsg), 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,228 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.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");
|
||||
}
|
||||
}
|
||||
}
|
128
MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs
Normal file
128
MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace MareSynchronos.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 _mareHub!.SendAsync(nameof(GroupChangeIndividualPermissionState), dto).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 GroupChatSendMsg(GroupDto group, ChatMessage message)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupChatSendMsg), group, message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupClear(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<GroupPasswordDto> GroupCreate()
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<GroupPasswordDto>(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<bool> GroupJoin(GroupPasswordDto passwordedGroup)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(GroupJoin), 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<List<GroupPairFullInfoDto>> GroupsGetUsersInGroup(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _mareHub!.InvokeAsync<List<GroupPairFullInfoDto>>(nameof(GroupsGetUsersInGroup), group).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");
|
||||
}
|
||||
}
|
482
MareSynchronos/WebAPI/SignalR/ApiController.cs
Normal file
482
MareSynchronos/WebAPI/SignalR/ApiController.cs
Normal file
@@ -0,0 +1,482 @@
|
||||
using Dalamud.Utility;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.WebAPI.SignalR;
|
||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MareSynchronos.WebAPI;
|
||||
|
||||
#pragma warning disable MA0040
|
||||
public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient
|
||||
{
|
||||
public const string ElezenServer = "Snowcloak Main Server";
|
||||
public const string ElezenServiceUri = "wss://hub.snowcloak-sync.com";
|
||||
public const string ElezenServiceApiUri = "wss://hub.snowcloak-sync.com/";
|
||||
public const string ElezenServiceHubUri = "wss://hub.snowcloak-sync.com/mare";
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly HubFactory _hubFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private CancellationTokenSource _connectionCancellationTokenSource;
|
||||
private ConnectionDto? _connectionDto;
|
||||
private bool _doNotNotifyOnNextInfo = false;
|
||||
private CancellationTokenSource? _healthCheckTokenSource = new();
|
||||
private bool _initialized;
|
||||
private HubConnection? _mareHub;
|
||||
private ServerState _serverState;
|
||||
private CensusUpdateMessage? _lastCensus;
|
||||
|
||||
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
|
||||
PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator,
|
||||
TokenProvider tokenProvider) : base(logger, mediator)
|
||||
{
|
||||
_hubFactory = hubFactory;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_serverManager = serverManager;
|
||||
_tokenProvider = tokenProvider;
|
||||
_connectionCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
|
||||
Mediator.Subscribe<HubClosedMessage>(this, (msg) => MareHubOnClosed(msg.Exception));
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = MareHubOnReconnected());
|
||||
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => MareHubOnReconnecting(msg.Exception));
|
||||
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePause(msg.UserData));
|
||||
Mediator.Subscribe<CensusUpdateMessage>(this, (msg) => _lastCensus = msg);
|
||||
Mediator.Subscribe<PauseMessage>(this, (msg) => _ = Pause(msg.UserData));
|
||||
|
||||
ServerState = ServerState.Offline;
|
||||
|
||||
if (_dalamudUtil.IsLoggedIn)
|
||||
{
|
||||
DalamudUtilOnLogIn();
|
||||
}
|
||||
}
|
||||
|
||||
public string AuthFailureMessage { get; private set; } = string.Empty;
|
||||
|
||||
public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0);
|
||||
|
||||
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
|
||||
|
||||
public bool IsConnected => ServerState == ServerState.Connected;
|
||||
|
||||
public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0));
|
||||
|
||||
public int OnlineUsers => SystemInfoDto.OnlineUsers;
|
||||
|
||||
public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected;
|
||||
|
||||
public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo();
|
||||
|
||||
public ServerState ServerState
|
||||
{
|
||||
get => _serverState;
|
||||
private set
|
||||
{
|
||||
Logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState);
|
||||
_serverState = value;
|
||||
}
|
||||
}
|
||||
|
||||
public SystemInfoDto SystemInfoDto { get; private set; } = new();
|
||||
|
||||
public string UID => _connectionDto?.User.UID ?? string.Empty;
|
||||
|
||||
public async Task<bool> CheckClientHealth()
|
||||
{
|
||||
return await _mareHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CreateConnections()
|
||||
{
|
||||
Logger.LogDebug("CreateConnections called");
|
||||
|
||||
if (_serverManager.CurrentServer?.FullPause ?? true)
|
||||
{
|
||||
Logger.LogInformation("Not recreating Connection, paused");
|
||||
_connectionDto = null;
|
||||
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
var secretKey = _serverManager.GetSecretKey(out bool multi);
|
||||
if (multi)
|
||||
{
|
||||
Logger.LogWarning("Multiple secret keys for current character");
|
||||
_connectionDto = null;
|
||||
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 StopConnection(ServerState.MultiChara).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (secretKey == null)
|
||||
{
|
||||
Logger.LogWarning("No secret key set for current character");
|
||||
_connectionDto = null;
|
||||
await StopConnection(ServerState.NoSecretKey).ConfigureAwait(false);
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
|
||||
|
||||
Logger.LogInformation("Recreating Connection");
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
|
||||
$"Starting Connection to {_serverManager.CurrentServer.ServerName}")));
|
||||
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
_connectionCancellationTokenSource?.Dispose();
|
||||
_connectionCancellationTokenSource = new CancellationTokenSource();
|
||||
var token = _connectionCancellationTokenSource.Token;
|
||||
while (ServerState is not ServerState.Connected && !token.IsCancellationRequested)
|
||||
{
|
||||
AuthFailureMessage = string.Empty;
|
||||
|
||||
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
|
||||
ServerState = ServerState.Connecting;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Building connection");
|
||||
|
||||
try
|
||||
{
|
||||
await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (MareAuthFailureException ex)
|
||||
{
|
||||
AuthFailureMessage = ex.Reason;
|
||||
throw new HttpRequestException("Error during authentication", ex, System.Net.HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false) && !token.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogDebug("Player not loaded in yet, waiting");
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested) break;
|
||||
|
||||
_mareHub = await _hubFactory.GetOrCreate(token).ConfigureAwait(false);
|
||||
InitializeApiHooks();
|
||||
|
||||
await _mareHub.StartAsync(token).ConfigureAwait(false);
|
||||
|
||||
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||
|
||||
ServerState = ServerState.Connected;
|
||||
|
||||
var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||
|
||||
if (_connectionDto.ServerVersion != IMareHub.ApiVersion)
|
||||
{
|
||||
if (_connectionDto.CurrentClientVersion > currentClientVer)
|
||||
{
|
||||
Mediator.Publish(new NotificationMessage("Client incompatible",
|
||||
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
|
||||
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
|
||||
$"This client version is incompatible and will not be able to connect. Please update your Elezen client.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_connectionDto.CurrentClientVersion > currentClientVer)
|
||||
{
|
||||
Mediator.Publish(new NotificationMessage("Client outdated",
|
||||
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
|
||||
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
|
||||
$"Please keep your Elezen client up-to-date.",
|
||||
NotificationType.Warning, TimeSpan.FromSeconds(15)));
|
||||
}
|
||||
|
||||
if (_dalamudUtil.HasModifiedGameFiles)
|
||||
{
|
||||
Logger.LogWarning("Detected modified game files on connection");
|
||||
#if false
|
||||
Mediator.Publish(new NotificationMessage("Modified Game Files detected",
|
||||
"Dalamud has reported modified game files in your FFXIV installation. " +
|
||||
"You will be able to connect, but the synchronization functionality might be (partially) broken. " +
|
||||
"Exit the game and repair it through XIVLauncher to get rid of this message.",
|
||||
NotificationType.Error, TimeSpan.FromSeconds(15)));
|
||||
#endif
|
||||
}
|
||||
|
||||
await LoadIninitialPairs().ConfigureAwait(false);
|
||||
await LoadOnlinePairs().ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning("Connection attempt cancelled");
|
||||
return;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "HttpRequestException on Connection");
|
||||
|
||||
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await StopConnection(ServerState.Unauthorized).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerState = ServerState.Reconnecting;
|
||||
Logger.LogInformation("Failed to establish connection, retrying");
|
||||
await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "InvalidOperationException on connection");
|
||||
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Exception on Connection");
|
||||
|
||||
Logger.LogInformation("Failed to establish connection, retrying");
|
||||
await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task CyclePause(UserData userData)
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData);
|
||||
var perm = pair.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
// wait until it's changed
|
||||
while (pair.UserPair!.OwnPermissions != perm)
|
||||
{
|
||||
await Task.Delay(250, cts.Token).ConfigureAwait(false);
|
||||
Logger.LogTrace("Waiting for permissions change for {data}", userData);
|
||||
}
|
||||
perm.SetPaused(paused: false);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
}, cts.Token).ContinueWith((t) => cts.Dispose());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Pause(UserData userData)
|
||||
{
|
||||
var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData);
|
||||
var perm = pair.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<ConnectionDto> GetConnectionDto() => GetConnectionDto(true);
|
||||
|
||||
public async Task<ConnectionDto> GetConnectionDto(bool publishConnected = true)
|
||||
{
|
||||
var dto = await _mareHub!.InvokeAsync<ConnectionDto>(nameof(GetConnectionDto)).ConfigureAwait(false);
|
||||
if (publishConnected) Mediator.Publish(new ConnectedMessage(dto));
|
||||
return dto;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
_ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
|
||||
_connectionCancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
private async Task ClientHealthCheck(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested && _mareHub != null)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
|
||||
Logger.LogDebug("Checking Client Health State");
|
||||
_ = await CheckClientHealth().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void DalamudUtilOnLogIn()
|
||||
{
|
||||
_ = Task.Run(() => CreateConnections());
|
||||
}
|
||||
|
||||
private void DalamudUtilOnLogOut()
|
||||
{
|
||||
_ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false));
|
||||
ServerState = ServerState.Offline;
|
||||
}
|
||||
|
||||
private void InitializeApiHooks()
|
||||
{
|
||||
if (_mareHub == null) return;
|
||||
|
||||
Logger.LogDebug("Initializing data");
|
||||
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
|
||||
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
|
||||
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
|
||||
|
||||
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
|
||||
OnUserAddClientPair((dto) => _ = Client_UserAddClientPair(dto));
|
||||
OnUserReceiveCharacterData((dto) => _ = Client_UserReceiveCharacterData(dto));
|
||||
OnUserRemoveClientPair(dto => _ = Client_UserRemoveClientPair(dto));
|
||||
OnUserSendOnline(dto => _ = Client_UserSendOnline(dto));
|
||||
OnUserUpdateOtherPairPermissions(dto => _ = Client_UserUpdateOtherPairPermissions(dto));
|
||||
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
|
||||
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
|
||||
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
|
||||
|
||||
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
|
||||
OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
|
||||
OnGroupPairChangeUserInfo((dto) => _ = Client_GroupPairChangeUserInfo(dto));
|
||||
OnGroupPairJoined((dto) => _ = Client_GroupPairJoined(dto));
|
||||
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
|
||||
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
|
||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||
OnGroupPairChangePermissions((dto) => _ = Client_GroupPairChangePermissions(dto));
|
||||
|
||||
OnUserChatMsg((dto) => _ = Client_UserChatMsg(dto));
|
||||
OnGroupChatMsg((dto) => _ = Client_GroupChatMsg(dto));
|
||||
|
||||
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
||||
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
|
||||
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
|
||||
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
|
||||
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
|
||||
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
_healthCheckTokenSource?.Dispose();
|
||||
_healthCheckTokenSource = new CancellationTokenSource();
|
||||
_ = ClientHealthCheck(_healthCheckTokenSource.Token);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private async Task LoadIninitialPairs()
|
||||
{
|
||||
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Individual Pair: {userPair}", userPair);
|
||||
_pairManager.AddUserPair(userPair, addToLastAddedUser: false);
|
||||
}
|
||||
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Group: {entry}", entry);
|
||||
_pairManager.AddGroup(entry);
|
||||
}
|
||||
foreach (var group in _pairManager.GroupPairs.Keys)
|
||||
{
|
||||
var users = await GroupsGetUsersInGroup(group).ConfigureAwait(false);
|
||||
foreach (var user in users)
|
||||
{
|
||||
Logger.LogDebug("Group Pair: {user}", user);
|
||||
_pairManager.AddGroupPair(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadOnlinePairs()
|
||||
{
|
||||
foreach (var entry in await UserGetOnlinePairs().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Pair online: {pair}", entry);
|
||||
_pairManager.MarkPairOnline(entry, sendNotif: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void MareHubOnClosed(Exception? arg)
|
||||
{
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
Mediator.Publish(new DisconnectedMessage());
|
||||
ServerState = ServerState.Offline;
|
||||
if (arg != null)
|
||||
{
|
||||
Logger.LogWarning(arg, "Connection closed");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("Connection closed");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MareHubOnReconnected()
|
||||
{
|
||||
ServerState = ServerState.Reconnecting;
|
||||
try
|
||||
{
|
||||
InitializeApiHooks();
|
||||
_connectionDto = await GetConnectionDto(publishConnected: false).ConfigureAwait(false);
|
||||
if (_connectionDto.ServerVersion != IMareHub.ApiVersion)
|
||||
{
|
||||
await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
ServerState = ServerState.Connected;
|
||||
await LoadIninitialPairs().ConfigureAwait(false);
|
||||
await LoadOnlinePairs().ConfigureAwait(false);
|
||||
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogCritical(ex, "Failure to obtain data after reconnection");
|
||||
await StopConnection(ServerState.Disconnected).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void MareHubOnReconnecting(Exception? arg)
|
||||
{
|
||||
_doNotNotifyOnNextInfo = true;
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
ServerState = ServerState.Reconnecting;
|
||||
Logger.LogWarning(arg, "Connection closed... Reconnecting");
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Warning,
|
||||
$"Connection interrupted, reconnecting to {_serverManager.CurrentServer.ServerName}")));
|
||||
|
||||
}
|
||||
|
||||
private async Task StopConnection(ServerState state)
|
||||
{
|
||||
ServerState = ServerState.Disconnecting;
|
||||
|
||||
Logger.LogInformation("Stopping existing connection");
|
||||
await _hubFactory.DisposeHubAsync().ConfigureAwait(false);
|
||||
|
||||
if (_mareHub is not null)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
|
||||
$"Stopping existing connection to {_serverManager.CurrentServer.ServerName}")));
|
||||
|
||||
_initialized = false;
|
||||
_healthCheckTokenSource?.Cancel();
|
||||
Mediator.Publish(new DisconnectedMessage());
|
||||
_mareHub = null;
|
||||
_connectionDto = null;
|
||||
}
|
||||
|
||||
ServerState = state;
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
50
MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs
Normal file
50
MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public record HubConnectionConfig
|
||||
{
|
||||
[JsonPropertyName("api_url")]
|
||||
public string ApiUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("hub_url")]
|
||||
public string HubUrl { get; set; } = string.Empty;
|
||||
|
||||
private readonly bool? _skipNegotiation;
|
||||
|
||||
[JsonPropertyName("skip_negotiation")]
|
||||
public bool SkipNegotiation
|
||||
{
|
||||
get => _skipNegotiation ?? true;
|
||||
init => _skipNegotiation = value;
|
||||
}
|
||||
|
||||
[JsonPropertyName("transports")]
|
||||
public string[]? Transports { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public HttpTransportType TransportType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Transports == null || Transports.Length == 0)
|
||||
return HttpTransportType.WebSockets;
|
||||
|
||||
HttpTransportType result = HttpTransportType.None;
|
||||
|
||||
foreach (var transport in Transports)
|
||||
{
|
||||
result |= transport.ToLowerInvariant() switch
|
||||
{
|
||||
"websockets" => HttpTransportType.WebSockets,
|
||||
"serversentevents" => HttpTransportType.ServerSentEvents,
|
||||
"longpolling" => HttpTransportType.LongPolling,
|
||||
_ => HttpTransportType.None
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
240
MareSynchronos/WebAPI/SignalR/HubFactory.cs
Normal file
240
MareSynchronos/WebAPI/SignalR/HubFactory.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.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;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public class HubFactory : MediatorSubscriberBase
|
||||
{
|
||||
private readonly ILoggerProvider _loggingProvider;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly RemoteConfigurationService _remoteConfig;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private HubConnection? _instance;
|
||||
private string _cachedConfigFor = string.Empty;
|
||||
private HubConnectionConfig? _cachedConfig;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator,
|
||||
ServerConfigurationManager serverConfigurationManager, RemoteConfigurationService remoteConfig,
|
||||
TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_remoteConfig = remoteConfig;
|
||||
_tokenProvider = tokenProvider;
|
||||
_loggingProvider = pluginLog;
|
||||
}
|
||||
|
||||
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 async Task<HubConnection> GetOrCreate(CancellationToken ct)
|
||||
{
|
||||
if (!_isDisposed && _instance != null) return _instance;
|
||||
|
||||
_cachedConfig = await ResolveHubConfig().ConfigureAwait(false);
|
||||
_cachedConfigFor = _serverConfigurationManager.CurrentApiUrl;
|
||||
|
||||
return BuildHubConnection(_cachedConfig, ct);
|
||||
}
|
||||
|
||||
private async Task<HubConnectionConfig> ResolveHubConfig()
|
||||
{
|
||||
var stapledWellKnown = _tokenProvider.GetStapledWellKnown(_serverConfigurationManager.CurrentApiUrl);
|
||||
|
||||
var apiUrl = new Uri(_serverConfigurationManager.CurrentApiUrl);
|
||||
|
||||
HubConnectionConfig defaultConfig;
|
||||
|
||||
if (_cachedConfig != null && _serverConfigurationManager.CurrentApiUrl.Equals(_cachedConfigFor, StringComparison.Ordinal))
|
||||
{
|
||||
defaultConfig = _cachedConfig;
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultConfig = new HubConnectionConfig
|
||||
{
|
||||
HubUrl = _serverConfigurationManager.CurrentApiUrl.TrimEnd('/') + IMareHub.Path,
|
||||
Transports = []
|
||||
};
|
||||
}
|
||||
|
||||
if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.ElezenServiceUri, StringComparison.Ordinal))
|
||||
{
|
||||
var mainServerConfig = await _remoteConfig.GetConfigAsync<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
|
||||
defaultConfig = mainServerConfig;
|
||||
if (string.IsNullOrEmpty(mainServerConfig.ApiUrl))
|
||||
defaultConfig.ApiUrl = ApiController.ElezenServiceApiUri;
|
||||
if (string.IsNullOrEmpty(mainServerConfig.HubUrl))
|
||||
defaultConfig.HubUrl = ApiController.ElezenServiceHubUri;
|
||||
}
|
||||
|
||||
string jsonResponse;
|
||||
|
||||
if (stapledWellKnown != null)
|
||||
{
|
||||
jsonResponse = stapledWellKnown;
|
||||
Logger.LogTrace("Using stapled hub config for {url}", _serverConfigurationManager.CurrentApiUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpScheme = apiUrl.Scheme.ToLowerInvariant() switch
|
||||
{
|
||||
"ws" => "http",
|
||||
"wss" => "https",
|
||||
_ => apiUrl.Scheme
|
||||
};
|
||||
|
||||
var wellKnownUrl = $"{httpScheme}://{apiUrl.Host}/.well-known/Elezen/client";
|
||||
Logger.LogTrace("Fetching hub config for {uri} via {wk}", _serverConfigurationManager.CurrentApiUrl, wellKnownUrl);
|
||||
|
||||
using var httpClient = new HttpClient(
|
||||
new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
MaxAutomaticRedirections = 5
|
||||
}
|
||||
);
|
||||
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
|
||||
var response = await httpClient.GetAsync(wellKnownUrl).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return defaultConfig;
|
||||
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType;
|
||||
|
||||
if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal))
|
||||
return defaultConfig;
|
||||
|
||||
jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "HTTP request failed for .well-known");
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<HubConnectionConfig>(jsonResponse);
|
||||
|
||||
if (config == null)
|
||||
return defaultConfig;
|
||||
|
||||
if (string.IsNullOrEmpty(config.ApiUrl))
|
||||
config.ApiUrl = defaultConfig.ApiUrl;
|
||||
|
||||
if (string.IsNullOrEmpty(config.HubUrl))
|
||||
config.HubUrl = defaultConfig.HubUrl;
|
||||
|
||||
config.Transports ??= defaultConfig.Transports ?? [];
|
||||
|
||||
return config;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Invalid JSON in .well-known response");
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private HubConnection BuildHubConnection(HubConnectionConfig hubConfig, CancellationToken ct)
|
||||
{
|
||||
Logger.LogDebug("Building new HubConnection");
|
||||
|
||||
_instance = new HubConnectionBuilder()
|
||||
.WithUrl(hubConfig.HubUrl, options =>
|
||||
{
|
||||
var transports = hubConfig.TransportType;
|
||||
options.AccessTokenProvider = () => _tokenProvider.GetOrUpdateToken(ct);
|
||||
options.SkipNegotiation = hubConfig.SkipNegotiation && (transports == HttpTransportType.WebSockets);
|
||||
options.Transports = transports;
|
||||
})
|
||||
.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
MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs
Normal file
9
MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public record JwtIdentifier(string ApiUrl, string CharaHash, string SecretKey)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return "{JwtIdentifier; Url: " + ApiUrl + ", Chara: " + CharaHash + ", HasSecretKey: " + !string.IsNullOrEmpty(SecretKey) + "}";
|
||||
}
|
||||
}
|
11
MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs
Normal file
11
MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace MareSynchronos.WebAPI.SignalR;
|
||||
|
||||
public class MareAuthFailureException : Exception
|
||||
{
|
||||
public MareAuthFailureException(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public string Reason { get; }
|
||||
}
|
197
MareSynchronos/WebAPI/SignalR/TokenProvider.cs
Normal file
197
MareSynchronos/WebAPI/SignalR/TokenProvider.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using MareSynchronos.API.Routes;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Utils;
|
||||
using MareSynchronos.API.Dto;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MareSynchronos.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 RemoteConfigurationService _remoteConfig;
|
||||
private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new();
|
||||
private readonly ConcurrentDictionary<string, string?> _wellKnownCache = new(StringComparer.Ordinal);
|
||||
|
||||
public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig,
|
||||
DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_serverManager = serverManager;
|
||||
_remoteConfig = remoteConfig;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_httpClient = new(
|
||||
new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
MaxAutomaticRedirections = 5
|
||||
}
|
||||
);
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
Mediator = mareMediator;
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) =>
|
||||
{
|
||||
_lastJwtIdentifier = null;
|
||||
_tokenCache.Clear();
|
||||
_wellKnownCache.Clear();
|
||||
});
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) =>
|
||||
{
|
||||
_lastJwtIdentifier = null;
|
||||
_tokenCache.Clear();
|
||||
_wellKnownCache.Clear();
|
||||
});
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
}
|
||||
|
||||
public MareMediator Mediator { get; }
|
||||
|
||||
private JwtIdentifier? _lastJwtIdentifier;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Mediator.UnsubscribeAll(this);
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
public async Task<string> GetNewToken(JwtIdentifier identifier, CancellationToken token)
|
||||
{
|
||||
Uri tokenUri;
|
||||
HttpResponseMessage result;
|
||||
|
||||
var authApiUrl = _serverManager.CurrentApiUrl;
|
||||
|
||||
// Override the API URL used for auth from remote config, if one is available
|
||||
if (authApiUrl.Equals(ApiController.ElezenServiceUri, StringComparison.Ordinal))
|
||||
{
|
||||
var config = await _remoteConfig.GetConfigAsync<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
|
||||
if (!string.IsNullOrEmpty(config.ApiUrl))
|
||||
authApiUrl = config.ApiUrl;
|
||||
else
|
||||
authApiUrl = ApiController.ElezenServiceApiUri;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("GetNewToken: Requesting");
|
||||
|
||||
tokenUri = MareAuth.AuthV2FullPath(new Uri(authApiUrl
|
||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||
var secretKey = _serverManager.GetSecretKey(out _)!;
|
||||
var auth = secretKey.GetHash256();
|
||||
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent([
|
||||
new("auth", auth),
|
||||
new("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
|
||||
]), token).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting manually.", NotificationType.Error));
|
||||
Mediator.Publish(new DisconnectedMessage());
|
||||
var textResponse = await result.Content.ReadAsStringAsync(token).ConfigureAwait(false) ?? string.Empty;
|
||||
throw new MareAuthFailureException(textResponse);
|
||||
}
|
||||
|
||||
var response = await result.Content.ReadFromJsonAsync<AuthReplyDto>(token).ConfigureAwait(false) ?? new();
|
||||
_tokenCache[identifier] = response.Token;
|
||||
_wellKnownCache[_serverManager.CurrentApiUrl] = response.WellKnown;
|
||||
return response.Token;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_tokenCache.TryRemove(identifier, out _);
|
||||
_wellKnownCache.TryRemove(_serverManager.CurrentApiUrl, out _);
|
||||
|
||||
_logger.LogError(ex, "GetNewToken: Failure to get token");
|
||||
|
||||
if (ex.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting manually.", NotificationType.Error));
|
||||
Mediator.Publish(new DisconnectedMessage());
|
||||
throw new MareAuthFailureException(ex.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
jwtIdentifier = new(_serverManager.CurrentApiUrl,
|
||||
playerIdentifier,
|
||||
_serverManager.GetSecretKey(out _)!);
|
||||
_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;
|
||||
|
||||
if (_tokenCache.TryGetValue(jwtIdentifier, out var token))
|
||||
return token;
|
||||
|
||||
_logger.LogTrace("GetOrUpdate: Getting new token");
|
||||
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string? GetStapledWellKnown(string apiUrl)
|
||||
{
|
||||
_wellKnownCache.TryGetValue(apiUrl, out var wellKnown);
|
||||
// Treat an empty string as null -- it won't decode as JSON anyway
|
||||
if (string.IsNullOrEmpty(wellKnown))
|
||||
return null;
|
||||
return wellKnown;
|
||||
}
|
||||
}
|
39
MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs
Normal file
39
MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace MareSynchronos.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;
|
||||
}
|
||||
}
|
16
MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs
Normal file
16
MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace MareSynchronos.WebAPI.SignalR.Utils;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
Offline,
|
||||
Connecting,
|
||||
Reconnecting,
|
||||
Disconnecting,
|
||||
Disconnected,
|
||||
Connected,
|
||||
Unauthorized,
|
||||
VersionMisMatch,
|
||||
RateLimited,
|
||||
NoSecretKey,
|
||||
MultiChara,
|
||||
}
|
Reference in New Issue
Block a user