using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using QuestShare.Common.API; using QuestShare.Common.API.Share; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.Json.Nodes; using System.Threading.Tasks; namespace QuestShare.Services { internal class SocketClientService { private readonly string socketUrl = "https://api.nathanc.tech/Hub"; internal static HubConnection connection { get; private set; } = null!; internal static bool IsConnected => connection.State == HubConnectionState.Connected; internal static bool IsAuthorized { get; private set; } = false; private static bool IsDisposing = false; public event EventHandler OnCancelEvent = delegate { }; public event EventHandler OnGroupEvent = delegate { }; public event EventHandler OnUngroupEvent = delegate { }; public event EventHandler OnGetEvent = delegate { }; public event EventHandler OnRegisterEvent = delegate { }; public event EventHandler OnUpdateEvent = delegate { }; public event EventHandler OnTokenCheckEvent = delegate { }; public SocketClientService() { var builder = new HubConnectionBuilder().WithUrl(socketUrl).ConfigureLogging(logging => { logging.SetMinimumLevel(LogLevel.Information).AddConsole(); }); connection = builder.Build(); connection.Closed += async (error) => { if (IsDisposing) return; Log.Warning($"Connection closed... {error}"); Log.Warning($"Connection closed, retrying... {error}"); await Task.Delay(new Random().Next(0, 5) * 1000); await connection.StartAsync(); }; connection.Reconnected += async (error) => { Log.Information("Connection reconnected"); }; connection.On(nameof(Register), Client_Register); connection.On(nameof(Update), Client_Update); connection.On(nameof(Update.UpdateBroadcast), Client_UpdateBroadcast); connection.On(nameof(GetShareInfo), Client_GetShareInfo); connection.On(nameof(Cancel), Client_Cancel); connection.On(nameof(Cancel.CancelBroadcast), Client_CancelBroadast); connection.On(nameof(GroupJoin), Client_GroupJoin); connection.On(nameof(GroupLeave), Client_GroupLeave); connection.On(nameof(Authorize), Client_Authorize); connection.On(nameof(GroupNotify.GroupNotifyBroadcast), Client_GroupNotify); connection.On(nameof(Resume), Client_Resume); connection.On(nameof(Authorize.AuthBroadcast), Client_AuthRequest); ClientState.Login += OnLogin; ClientState.Logout += OnLogout; } public void Dispose() { IsDisposing = true; if (IsConnected) { connection.StopAsync(); } connection.DisposeAsync(); ClientState.Login -= OnLogin; ClientState.Logout -= OnLogout; } public void OnLogin() { if (Plugin.Configuration.Instance.Token != "") { ShareService.Token = Plugin.Configuration.Instance.Token; DispatchAuthorize(); } } public void OnLogout(int code, int type) { ShareService.Token = ""; Plugin.Configuration.Save(); IsAuthorized = false; connection.StopAsync().ConfigureAwait(false); } public async Task Connect() { try { if (IsConnected) await connection.StopAsync(); IsDisposing = false; await connection.StartAsync().ContinueWith(task => { if (task.IsFaulted) { Log.Error("Failed to connect to socket server"); } else { Log.Information("Connected to socket server"); } }); } catch (Exception ex) { Log.Error(ex.Message); } } public async Task Disconnect() { IsDisposing = true; await connection.StopAsync(); } private async Task Invoke(string methodName, object request) { if (!IsConnected) await Connect(); Log.Debug($"Invoking {methodName} with {JsonConvert.SerializeObject(request)}"); var s = new StackTrace(); Log.Debug(s.ToString()); await connection.InvokeAsync(methodName, request); } public static void DispatchAuthorize() { Plugin.SocketClient.Invoke(nameof(Authorize), new Authorize.Request { Version = Constants.Version, Token = ShareService.Token, CharacterId = ClientState.LocalContentId }).ConfigureAwait(false); } public static void DispatchRegister() { if (!IsAuthorized) { Log.Warning("Not authorized to register"); return; } var activeQuest = GameQuestManager.GetActiveQuest(); Plugin.SocketClient.Invoke(nameof(Register), new Register.Request { Version = Constants.Version, Token = ShareService.Token, CharacterId = ClientState.LocalContentId, SharedQuestId = activeQuest != null ? activeQuest.QuestId : 0, SharedQuestStep = activeQuest != null ? activeQuest.CurrentStep : (byte)0, }).ConfigureAwait(false); } public static void DispatchGroup(string shareCode) { Plugin.SocketClient.Invoke(nameof(GroupJoin), new GroupJoin.Request { Token = ShareService.Token, ShareCode = shareCode, Version = Constants.Version, CharacterId = ClientState.LocalContentId }).ConfigureAwait(false); } public static void DispatchUngroup() { Plugin.SocketClient.Invoke(nameof(GroupLeave), new GroupLeave.Request { Token = ShareService.Token, ShareCode = ShareService.ShareCode, Version = Constants.Version, }).ConfigureAwait(false); } public static void DispatchResume() { var members = new List(); foreach (var member in PartyList) { members.Add((ulong)member.ContentId); } Plugin.SocketClient.Invoke(nameof(Resume), new Resume.Request { Token = ShareService.Token, Version = Constants.Version, Members = members, ShareCode = Plugin.Configuration.Instance.LastShareCode, }).ConfigureAwait(false); } public static void DispatchGetShareInfo() { Plugin.SocketClient.Invoke(nameof(GetShareInfo), new GetShareInfo.Request { Token = ShareService.Token, Version = Constants.Version, ShareCode = ShareService.ShareCode }).ConfigureAwait(false); } public static void DispatchCancel() { Plugin.SocketClient.Invoke(nameof(Cancel), new Cancel.Request { Token = ShareService.Token, Version = Constants.Version, ShareCode = ShareService.HostedShareCode }).ConfigureAwait(false); } public static void DispatchUpdate(bool partyUpdate) { if (!ShareService.IsRegistered || !ShareService.IsGrouped) { return; } var activeQuest = GameQuestManager.GetActiveQuest(); Plugin.SocketClient.Invoke(nameof(Update), new Update.Request { Token = ShareService.Token, Version = Constants.Version, SharedQuestId = activeQuest != null ? activeQuest.QuestId : 0, SharedQuestStep = activeQuest != null ? activeQuest.CurrentStep : (byte)0, BroadcastParty = Plugin.Configuration.Instance.BroadcastToParty, PartyMembers = ShareService.IsHost ? ShareService.PartyMembers : [], IsPartyChange = partyUpdate }).ConfigureAwait(false); } #region Server Methods private Task Client_Register(Register.Response response) { Log.Debug($"Client_Register({JsonConvert.SerializeObject(response)})"); if (response.Success) { ShareService.SetHostedShareCode(response.ShareCode); ShareService.IsHost = true; ShareService.IsGrouped = false; OnRegisterEvent.Invoke(this, new SocketEventArgs { Success = true }); } else { OnRegisterEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_Update(Update.Response response) { Log.Debug($"Client_Update({JsonConvert.SerializeObject(response)})"); if (response.Success) { OnUpdateEvent.Invoke(this, new SocketEventArgs { Success = true }); } else { OnUpdateEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_UpdateBroadcast(Update.UpdateBroadcast broadcast) { Log.Debug($"Client_UpdateBroadcast({JsonConvert.SerializeObject(broadcast)})"); ShareService.SetActiveQuest(broadcast.SharedQuestId, broadcast.SharedQuestStep); Log.Debug($"Updated quest: {broadcast.SharedQuestId} - {broadcast.SharedQuestStep}"); return Task.CompletedTask; } private Task Client_GroupJoin(GroupJoin.Response response) { Log.Debug($"Client_GroupJoin({JsonConvert.SerializeObject(response)})"); if (response.Success) { Plugin.Configuration.Instance.LastShareCode = response.ShareCode; Plugin.Configuration.Save(); ShareService.IsHost = false; ShareService.IsGrouped = true; ShareService.SetActiveQuest(response.SharedQuestId, response.SharedQuestStep); ShareService.SetShareCode(response.ShareCode); OnGroupEvent.Invoke(this, new SocketEventArgs { Success = true }); Log.Debug($"Joined group: {response.ShareCode}"); } else { Log.Error($"Failed to join group: {response.Error}"); OnGroupEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_GroupLeave(GroupLeave.Response response) { Log.Debug($"Client_GroupLeave({JsonConvert.SerializeObject(response)})"); if (response.Success) { ShareService.IsGrouped = false; ShareService.IsHost = false; ShareService.ActiveQuestId = 0; ShareService.ActiveQuestStep = 0; OnUngroupEvent.Invoke(this, new SocketEventArgs { Success = true }); Log.Debug("Left group"); } else { Log.Error($"Failed to leave group: {response.Error}"); OnUngroupEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_Resume(Resume.Response response) { Log.Debug($"Client_Resume({JsonConvert.SerializeObject(response)})"); if (response.Success) { if (response.IsHost) { ShareService.PartyMembers = response.Members ?? []; ShareService.IsHost = response.IsHost; ShareService.IsGrouped = false; ShareService.SetHostedShareCode(response.ShareCode); GameQuestManager.SetActiveFlag(response.SharedQuestId); } else { ShareService.PartyMembers = response.Members ?? []; ShareService.IsHost = false; ShareService.IsGrouped = true; ShareService.SetActiveQuest(response.SharedQuestId, response.SharedQuestStep); } OnGetEvent.Invoke(this, new SocketEventArgs { Success = true }); Log.Debug($"Resumed share: {response.ShareCode}"); DispatchUpdate(false); } else { Log.Warning($"Failed to resume share: {response.Error}"); ShareService.ActiveQuestId = 0; ShareService.ActiveQuestStep = 0; ShareService.SetShareCode(""); ShareService.IsGrouped = false; ShareService.IsHost = false; ShareService.PartyMembers = []; OnGetEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_Authorize(Authorize.Response response) { Log.Debug($"Client_Authorize({JsonConvert.SerializeObject(response)})"); if (response.Success) { ShareService.Token = response.Token; Plugin.Configuration.Instance.Token = response.Token; Plugin.Configuration.Save(); Log.Debug("Logged in"); IsAuthorized = true; } else { Log.Error($"Failed to authorize: {response.Error}"); } DispatchResume(); return Task.CompletedTask; } private Task Client_AuthRequest(Authorize.AuthBroadcast _) { Log.Debug("Received auth request"); DispatchAuthorize(); return Task.CompletedTask; } private Task Client_Cancel(Cancel.Response response) { Log.Debug($"Client_Cancel({JsonConvert.SerializeObject(response)})"); if (response.Success) { ShareService.SetActiveQuest(0, 0); ShareService.SetShareCode(""); ShareService.IsGrouped = false; ShareService.IsHost = false; OnCancelEvent.Invoke(this, new SocketEventArgs { Success = true }); Log.Debug("Cancelled share"); } else { Log.Error($"Failed to cancel share: {response.Error}"); ShareService.SetActiveQuest(0, 0); ShareService.SetShareCode(""); ShareService.IsGrouped = false; ShareService.IsHost = false; OnCancelEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_CancelBroadast(Cancel.CancelBroadcast broadcast) { Log.Debug($"Client_CancelBroadcast({JsonConvert.SerializeObject(broadcast)})"); ShareService.SetActiveQuest(0, 0); ShareService.SetShareCode(""); ShareService.IsGrouped = false; ShareService.IsHost = false; OnCancelEvent.Invoke(this, new SocketEventArgs { Success = true }); Log.Debug("Cancelled share"); return Task.CompletedTask; } private Task Client_GetShareInfo(GetShareInfo.Response response) { Log.Debug($"Client_GetShareInfo({JsonConvert.SerializeObject(response)})"); if (response.Success) { ShareService.SetActiveQuest(response.SharedQuestId, response.SharedQuestStep); ShareService.PartyMembers = response.Members ?? []; OnGetEvent.Invoke(this, new SocketEventArgs { Success = true }); Log.Debug($"Received share info: {response}"); } else { Log.Error($"Failed to get share info: {response.Error}"); OnGetEvent.Invoke(this, new SocketEventArgs { Success = false, Message = response.Error.ToString() }); } return Task.CompletedTask; } private Task Client_GroupNotify(GroupNotify.GroupNotifyBroadcast broadcast) { Log.Debug($"Client_GroupNotify({JsonConvert.SerializeObject(broadcast)})"); if (broadcast.NotifyType == NotifyType.Join) { ShareService.SharedMembers.Add(new ShareService.SharedMember { CharacterId = broadcast.CharacterId }); } else if (broadcast.NotifyType == NotifyType.Leave) { ShareService.SharedMembers.RemoveAll(m => m.CharacterId == broadcast.CharacterId); } else if (broadcast.NotifyType == NotifyType.JoinViaParty) { ShareService.SharedMembers.Add(new ShareService.SharedMember { CharacterId = broadcast.CharacterId }); } else if (broadcast.NotifyType == NotifyType.Rejoin) { ShareService.SharedMembers.Add(new ShareService.SharedMember { CharacterId = broadcast.CharacterId }); } return Task.CompletedTask; } #endregion public class SocketEventArgs : EventArgs { public bool Success { get; set; } public string Message { get; set; } = ""; } public class TokenCheckEventArgs : EventArgs { public bool TokenValid { get; set; } public bool ShareCodeValid { get; set; } } } }