using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.RoomBridges; using Serilog.Events; using WebSocketSharp; using WebSocketSharp.Server; using ErrorEventArgs = WebSocketSharp.ErrorEventArgs; namespace PepperDash.Essentials.WebSocketServer { /// /// Represents the behaviour to associate with a UiClient for WebSocket communication /// public class UiClient : WebSocketBehavior, IKeyed { /// public string Key { get; private set; } /// /// Client ID used by client for this connection /// public string Id { get; private set; } /// /// Updates the client ID - only accessible from within the assembly (e.g., by the server) /// /// The new client ID internal void UpdateId(string newId) { Id = newId; } /// /// Token associated with this client /// public string Token { get; private set; } /// /// The URL token key used to connect (from UiClientContexts dictionary key) /// public string TokenKey { get; set; } /// /// Touchpanel Key associated with this client /// public string TouchpanelKey { get; private set; } /// /// Gets or sets the mobile control system controller that handles this client's messages /// public MobileControlSystemController Controller { get; set; } /// /// Gets or sets the server instance for client registration /// public MobileControlWebsocketServer Server { get; set; } /// /// Gets or sets the room key that this client is associated with /// public string RoomKey { get; set; } /// /// The timestamp when this client connection was established /// private DateTime _connectionTime; /// /// Gets the duration that this client has been connected. Returns zero if not currently connected. /// public TimeSpan ConnectedDuration { get { if (Context.WebSocket.IsAlive) { return DateTime.Now - _connectionTime; } else { return new TimeSpan(0); } } } /// /// Triggered when this client closes it's connection /// public event EventHandler ConnectionClosed; /// /// Initializes a new instance of the UiClient class with the specified key /// /// The unique key to identify this client /// The client ID used by the client for this connection /// The token associated with this client /// The touchpanel key associated with this client public UiClient(string key, string id, string token, string touchpanelKey = "") { Key = key; Id = id; Token = token; TouchpanelKey = touchpanelKey; } /// protected override void OnOpen() { base.OnOpen(); _connectionTime = DateTime.Now; Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); Log.Level = LogLevel.Trace; // Get clientId from query parameter var queryString = Context.QueryString; var clientId = queryString["clientId"]; if (!string.IsNullOrEmpty(clientId)) { // New behavior: Validate and register with the server using provided clientId if (Server == null || !Server.RegisterUiClient(this, clientId, TokenKey)) { this.LogError("Failed to register client with ID {clientId}. Invalid or expired registration.", clientId); Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "Invalid or expired clientId"); return; } // Update this client's ID to the validated one Id = clientId; Key = $"uiclient-{TokenKey}-{RoomKey}-{clientId}"; this.LogInformation("Client {clientId} successfully connected and registered (new flow)", clientId); } else { // Legacy behavior: Use clientId from Token.Id (generated in HandleJoinRequest) this.LogInformation("Client connected without clientId query parameter. Using legacy registration flow."); // Id is already set from Token in constructor, use it if (string.IsNullOrEmpty(Id)) { this.LogError("Legacy client has no ID from token. Connection will be closed."); Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "No client ID available"); return; } Key = $"uiclient-{TokenKey}-{RoomKey}-{Id}"; // Register directly to active clients (legacy flow) if (Server != null) { Server.RegisterLegacyUiClient(this); } this.LogInformation("Client {clientId} registered using legacy flow", Id); } if (Controller == null) { Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null"); _connectionTime = DateTime.Now; } var clientJoinedMessage = new MobileControlMessage { Type = "/system/clientJoined", Content = JToken.FromObject(new { clientId = Id, roomKey = RoomKey, touchpanelKey = TouchpanelKey ?? string.Empty, }) }; Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage)); var bridge = Controller.GetRoomBridge(RoomKey); if (bridge == null) return; SendUserCodeToClient(bridge, Id); bridge.UserCodeChanged -= Bridge_UserCodeChanged; bridge.UserCodeChanged += Bridge_UserCodeChanged; // TODO: Future: Check token to see if there's already an open session using that token and reject/close the session } /// /// Handles the UserCodeChanged event from a room bridge and sends the updated user code to the client /// /// The room bridge that raised the event /// Event arguments private void Bridge_UserCodeChanged(object sender, EventArgs e) { SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, Id); } /// /// Sends the current user code and QR code URL to the specified client /// /// The room bridge containing the user code information /// The ID of the client to send the information to private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId) { var content = new { userCode = bridge.UserCode, qrUrl = bridge.QrCodeUrl, }; var message = new MobileControlMessage { Type = "/system/userCodeChanged", ClientId = clientId, Content = JToken.FromObject(content) }; Controller.SendMessageObjectToDirectClient(message); } /// protected override void OnMessage(MessageEventArgs e) { base.OnMessage(e); if (e.IsText && e.Data.Length > 0 && Controller != null) { // Forward the message to the controller to be put on the receive queue Controller.HandleClientMessage(e.Data); } } /// protected override void OnClose(CloseEventArgs e) { base.OnClose(e); this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); foreach (var messenger in Controller.Messengers) { messenger.Value.UnsubscribeClient(Id); } foreach (var messenger in Controller.DefaultMessengers) { messenger.Value.UnsubscribeClient(Id); } ConnectionClosed?.Invoke(this, new ConnectionClosedEventArgs(Id)); } /// protected override void OnError(ErrorEventArgs e) { base.OnError(e); this.LogError("WebSocket UiClient Error: {message}", e.Message); this.LogDebug(e.Exception, "Stack Trace"); } } }