using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using WebSocketSharp;
public class ChatClient
{
// Event Codes
private const byte EventChannels = 0;
private const byte EventSubscribe = 1;
private const byte EventUnSubscribe = 2;
private const byte EventPublicMessage = 3;
private const byte EventPrivateMessage = 4;
private const byte EventPlayerOnline = 5;
private const byte EventNotifyMessage = 97;
private const byte EventError = 99;
public bool IsConnected => _isConnected;
public string CurrentChannel => _currentChannel;
private readonly IChatListener _listener;
private WebSocket _webSocket;
private readonly Queue<Action> _mainThreadActions = new Queue<Action>();
private readonly string _userUniqueId;
private string _userName;
private string _currentChannel;
private bool _isConnected;
public ChatClient(IChatListener listener, string userName = "")
{
_listener = listener;
_userName = userName;
_userUniqueId = DataManager.Instance.UUID;
}
#region Public API
public void Connect(ChatServerItem server)
{
if (_isConnected) return;
string protocol = server.Secure == "Y" ? "wss" : "ws";
ConnectToServer(BuildServerUrl($"{protocol}://{server.Addr}:{server.Port}"));
}
public void Disconnect()
{
if (_webSocket == null) return;
_isConnected = false;
_webSocket.CloseAsync();
_webSocket = null;
}
public void Subscribe(string channel, int prevMessageCount = 0)
{
_currentChannel = channel;
Send(EventSubscribe, channel, null, null, prevMessageCount);
}
public void Unsubscribe(string channel) => Send(EventUnSubscribe, channel);
public void GetChannels() => Send(EventChannels, "");
public void GetPlayersOnline(string[] userIds)
{
if (_webSocket == null || !_webSocket.IsAlive)
{
_listener?.OnError("NOT_CONNECTED", "WebSocket is not connected");
return;
}
var msg = new ChatMessageModel
{
mid = Guid.NewGuid().ToString(),
type = EventPlayerOnline,
gameId = HttpClient.GetGameId,
serviceKey = HttpClient.GetServiceKey,
userUniqueId = _userUniqueId,
userName = _userName,
onlinePlayers = userIds,
ipAddr = GetLocalIP(),
createdAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")
};
_webSocket.Send(JsonUtility.ToJson(msg));
}
public void SendPublicMessage(string channel, string text)
{
Send(EventPublicMessage, channel, text);
}
public void SendPrivateMessage(string targetUserId, string text) => Send(EventPrivateMessage, _currentChannel, text, targetUserId);
public void SetUserName(string userName) => _userName = userName;
public void Service()
{
lock (_mainThreadActions)
{
while (_mainThreadActions.Count > 0)
_mainThreadActions.Dequeue()?.Invoke();
}
}
#endregion
#region Server Connection
private string BuildServerUrl(string baseUrl)
{
string hash = ComputeHash();
return $"{baseUrl}?gameId={HttpClient.GetGameId}&uuid={_userUniqueId}&hash={UnityWebRequest.EscapeURL(hash)}";
}
private void ConnectToServer(string url)
{
try
{
_webSocket = new WebSocket(url);
_webSocket.OnOpen += (s, e) => { _isConnected = true; RunOnMainThread(() => _listener?.OnConnected()); };
_webSocket.OnMessage += (s, e) => RunOnMainThread(() => ProcessMessage(e.Data));
_webSocket.OnError += (s, e) => RunOnMainThread(() => _listener?.OnError("WEBSOCKET_ERROR", e.Message));
_webSocket.OnClose += (s, e) => { _isConnected = false; RunOnMainThread(() => _listener?.OnDisconnected()); };
_webSocket.ConnectAsync();
}
catch (Exception e)
{
_listener?.OnError("CONNECTION_FAILED", e.Message);
}
}
#endregion
#region Message Handling
private void Send(byte type, string channel, string message = null, string privateToUserId = null, int prevMessageCount = 0)
{
if (_webSocket == null || !_webSocket.IsAlive)
{
_listener?.OnError("NOT_CONNECTED", "WebSocket is not connected");
return;
}
var msg = new ChatMessageModel
{
mid = Guid.NewGuid().ToString(),
type = type,
gameId = HttpClient.GetGameId,
serviceKey = HttpClient.GetServiceKey,
channelId = channel,
userUniqueId = _userUniqueId,
userName = _userName,
message = message,
privateToUserUniqueId = privateToUserId,
prevMessageCount = prevMessageCount,
ipAddr = GetLocalIP(),
createdAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")
};
_webSocket.Send(JsonUtility.ToJson(msg));
}
private void ProcessMessage(string json)
{
try
{
var msg = JsonUtility.FromJson<ChatMessageModel>(json);
var user = new ChatUserInfo { visitorId = msg.userUniqueId, visitorName = msg.userName };
switch (msg.type)
{
case EventChannels: _listener?.OnChannels(msg.channels); break;
case EventSubscribe: _listener?.OnSubscribed(user); break;
case EventUnSubscribe: _listener?.OnUnSubscribed(user); break;
case EventPublicMessage: _listener?.OnPublicMessage(user, msg.message); break;
case EventPrivateMessage: _listener?.OnPrivateMessage(user, msg.message); break;
case EventPlayerOnline: _listener?.OnPlayerOnline(msg.players); break;
case EventNotifyMessage: _listener?.OnNotifyMessage(user, msg.message); break;
case EventError: _listener?.OnError(msg.error.ToString(), msg.message); break;
}
}
catch (Exception e)
{
Debug.LogError($"[Chat] ProcessMessage error: {e.Message}");
}
}
private void RunOnMainThread(Action action)
{
lock (_mainThreadActions)
_mainThreadActions.Enqueue(action);
}
#endregion
#region Utilities
private string ComputeHash()
{
string message = $"{HttpClient.GetGameId}{HttpClient.GetServiceKey}{_userUniqueId}";
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(HttpClient.GetSecretKey)))
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)));
}
private string GetLocalIP()
{
try
{
foreach (var ip in System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList)
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
return ip.ToString();
}
catch { }
return "0.0.0.0";
}
#endregion
}