Skip to main content

ChatClient

WebSocket-based chat client core class. Handles actual server connection and message processing.

Event Codes

CodeConstant NameDescription
0EventChannelsChannel list
1EventSubscribeChannel subscribe
2EventUnSubscribeChannel unsubscribe
3EventPublicMessagePublic message
4EventPrivateMessagePrivate message
5EventPlayerOnlinePlayer online status
97EventNotifyMessageNotification message
99EventErrorError

Main Features

MethodDescription
ConnectConnect to WebSocket server
DisconnectDisconnect from server
SubscribeSubscribe to channel (can set previous message count)
UnsubscribeUnsubscribe from channel
GetChannelsRequest channel list
GetPlayersOnlineGet player online status
SendPublicMessageSend public message
SendPrivateMessageSend private message
SetUserNameSet user name
ServiceProcess main thread callbacks

Properties

PropertyTypeDescription
IsConnectedboolConnection status
CurrentChannelFStringCurrently subscribed channel

Unreal C++ Implementation

Header File (ChatClient.h)

// ChatClient.h
#pragma once

#include "CoreMinimal.h"
#include "IWebSocket.h"
#include "ChatModels.h"

class FChatClient
{
public:
// Event codes
static const uint8 EventChannels = 0;
static const uint8 EventSubscribe = 1;
static const uint8 EventUnSubscribe = 2;
static const uint8 EventPublicMessage = 3;
static const uint8 EventPrivateMessage = 4;
static const uint8 EventPlayerOnline = 5;
static const uint8 EventNotifyMessage = 97;
static const uint8 EventError = 99;

FChatClient(IChatListener* InListener, const FString& InUserName = TEXT(""));
~FChatClient();

// Public API
void Connect(const FChatServerItem& Server);
void Disconnect();
void Subscribe(const FString& Channel, int32 PrevMessageCount = 0);
void Unsubscribe(const FString& Channel);
void GetChannels();
void GetPlayersOnline(const TArray<FString>& UserIds);
void SendPublicMessage(const FString& Channel, const FString& Text);
void SendPrivateMessage(const FString& TargetUserId, const FString& Text);
void SetUserName(const FString& InUserName);

// Getters
bool GetIsConnected() const { return bIsConnected; }
FString GetCurrentChannel() const { return CurrentChannel; }

private:
void ConnectToServer(const FString& Url);
FString BuildServerUrl(const FString& BaseUrl);
void Send(uint8 Type, const FString& Channel, const FString& Message = TEXT(""),
const FString& PrivateToUserId = TEXT(""), int32 PrevMessageCount = 0);
void ProcessMessage(const FString& Json);
FString ComputeHash();
FString GetLocalIP();

// WebSocket event handlers
void OnConnected();
void OnConnectionError(const FString& Error);
void OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean);
void OnMessage(const FString& Message);

IChatListener* Listener;
TSharedPtr<IWebSocket> WebSocket;
FString UserUniqueId;
FString UserName;
FString CurrentChannel;
bool bIsConnected;
};

Implementation File (ChatClient.cpp)

OpenSSL Dependency

Uses OpenSSL for HMAC-SHA256 calculation. You must add the "OpenSSL" module to your project's Build.cs file.

// ChatClient.cpp
#include "ChatClient.h"
#include "WebSocketsModule.h"
#include "PlayNANOOAuth.h"
#include "Misc/Base64.h"
#include "SocketSubsystem.h"
#include "IPAddress.h"

#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#endif
#include <openssl/hmac.h>
#include <openssl/sha.h>
#if PLATFORM_WINDOWS
#include "Windows/HideWindowsPlatformTypes.h"
#endif

FChatClient::FChatClient(IChatListener* InListener, const FString& InUserName)
: Listener(InListener)
, UserName(InUserName)
, bIsConnected(false)
{
// UUID is assumed to be retrieved from DataManager
UserUniqueId = UGameDataManager::Get()->GetAccessToken(); // Or separate UUID management
}

FChatClient::~FChatClient()
{
Disconnect();
}

void FChatClient::Connect(const FChatServerItem& Server)
{
if (bIsConnected) return;

FString Protocol = Server.Secure == TEXT("Y") ? TEXT("wss") : TEXT("ws");
FString BaseUrl = FString::Printf(TEXT("%s://%s:%d"), *Protocol, *Server.Addr, Server.Port);
ConnectToServer(BuildServerUrl(BaseUrl));
}

void FChatClient::Disconnect()
{
if (WebSocket.IsValid())
{
bIsConnected = false;
WebSocket->Close();
WebSocket.Reset();
}
}

void FChatClient::Subscribe(const FString& Channel, int32 PrevMessageCount)
{
CurrentChannel = Channel;
Send(EventSubscribe, Channel, TEXT(""), TEXT(""), PrevMessageCount);
}

void FChatClient::Unsubscribe(const FString& Channel)
{
Send(EventUnSubscribe, Channel);
}

void FChatClient::GetChannels()
{
Send(EventChannels, TEXT(""));
}

void FChatClient::GetPlayersOnline(const TArray<FString>& UserIds)
{
if (!WebSocket.IsValid() || !bIsConnected)
{
if (Listener)
Listener->OnError(TEXT("NOT_CONNECTED"), TEXT("WebSocket is not connected"));
return;
}

FChatMessageModel Msg;
Msg.mid = FGuid::NewGuid().ToString();
Msg.type = EventPlayerOnline;
Msg.gameId = FPlayNANOOAuth::GameId;
Msg.serviceKey = FPlayNANOOAuth::ServiceKey;
Msg.userUniqueId = UserUniqueId;
Msg.userName = UserName;
Msg.onlinePlayers = UserIds;
Msg.ipAddr = GetLocalIP();
Msg.createdAt = FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));

WebSocket->Send(Msg.ToJson());
}

void FChatClient::SendPublicMessage(const FString& Channel, const FString& Text)
{
Send(EventPublicMessage, Channel, Text);
}

void FChatClient::SendPrivateMessage(const FString& TargetUserId, const FString& Text)
{
Send(EventPrivateMessage, CurrentChannel, Text, TargetUserId);
}

void FChatClient::SetUserName(const FString& InUserName)
{
UserName = InUserName;
}

FString FChatClient::BuildServerUrl(const FString& BaseUrl)
{
FString Hash = ComputeHash();
// Unreal WebSocket has a bug where it requests '//' if there's no path
// Explicitly add '/' path
return FString::Printf(TEXT("%s/?gameId=%s&uuid=%s&hash=%s"),
*BaseUrl,
*FPlayNANOOAuth::GameId,
*UserUniqueId,
*FGenericPlatformHttp::UrlEncode(Hash));
}

void FChatClient::ConnectToServer(const FString& Url)
{
if (!FModuleManager::Get().IsModuleLoaded(TEXT("WebSockets")))
{
FModuleManager::Get().LoadModule(TEXT("WebSockets"));
}

WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url);

WebSocket->OnConnected().AddLambda([this]()
{
OnConnected();
});

WebSocket->OnConnectionError().AddLambda([this](const FString& Error)
{
OnConnectionError(Error);
});

WebSocket->OnClosed().AddLambda([this](int32 StatusCode, const FString& Reason, bool bWasClean)
{
OnClosed(StatusCode, Reason, bWasClean);
});

WebSocket->OnMessage().AddLambda([this](const FString& Message)
{
OnMessage(Message);
});

WebSocket->Connect();
}

void FChatClient::OnConnected()
{
bIsConnected = true;
if (Listener)
Listener->OnConnected();
}

void FChatClient::OnConnectionError(const FString& Error)
{
if (Listener)
Listener->OnError(TEXT("CONNECTION_FAILED"), Error);
}

void FChatClient::OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
{
bIsConnected = false;
if (Listener)
Listener->OnDisconnected();
}

void FChatClient::OnMessage(const FString& Message)
{
ProcessMessage(Message);
}

void FChatClient::Send(uint8 Type, const FString& Channel, const FString& Message,
const FString& PrivateToUserId, int32 PrevMessageCount)
{
if (!WebSocket.IsValid() || !bIsConnected)
{
if (Listener)
Listener->OnError(TEXT("NOT_CONNECTED"), TEXT("WebSocket is not connected"));
return;
}

FChatMessageModel Msg;
Msg.mid = FGuid::NewGuid().ToString();
Msg.type = Type;
Msg.gameId = FPlayNANOOAuth::GameId;
Msg.serviceKey = FPlayNANOOAuth::ServiceKey;
Msg.channelId = Channel;
Msg.userUniqueId = UserUniqueId;
Msg.userName = UserName;
Msg.message = Message;
Msg.privateToUserUniqueId = PrivateToUserId;
Msg.prevMessageCount = PrevMessageCount;
Msg.ipAddr = GetLocalIP();
Msg.createdAt = FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));

WebSocket->Send(Msg.ToJson());
}

void FChatClient::ProcessMessage(const FString& Json)
{
UE_LOG(LogTemp, Log, TEXT("[Chat] Received: %s"), *Json);

FChatMessageModel Msg;
if (!Msg.FromJson(Json))
{
UE_LOG(LogTemp, Error, TEXT("[Chat] ProcessMessage error: Failed to parse JSON"));
return;
}

FChatUserInfo User;
User.visitorId = Msg.userUniqueId;
User.visitorName = Msg.userName;

if (!Listener) return;

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:
if (Msg.userUniqueId != UserUniqueId)
Listener->OnNotifyMessage(User, Msg.message);
break;
case EventError:
Listener->OnError(FString::FromInt(Msg.error), Msg.message);
break;
}
}

FString FChatClient::ComputeHash()
{
// Generate HMAC-SHA256 hash
FString Message = FPlayNANOOAuth::GameId + FPlayNANOOAuth::ServiceKey + UserUniqueId;

// Convert to UTF-8
FTCHARToUTF8 KeyUtf8(*FPlayNANOOAuth::SecretKey);
FTCHARToUTF8 MessageUtf8(*Message);

// Calculate HMAC-SHA256 (OpenSSL)
unsigned char Hash[SHA256_DIGEST_LENGTH];
unsigned int HashLen = 0;

HMAC(EVP_sha256(),
KeyUtf8.Get(), KeyUtf8.Length(),
(const unsigned char*)MessageUtf8.Get(), MessageUtf8.Length(),
Hash, &HashLen);

// Base64 encoding
TArray<uint8> HashArray;
HashArray.Append(Hash, HashLen);
return FBase64::Encode(HashArray);
}

FString FChatClient::GetLocalIP()
{
bool bCanBindAll;
TSharedPtr<FInternetAddr> LocalAddr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetLocalHostAddr(*GLog, bCanBindAll);
if (LocalAddr.IsValid())
{
return LocalAddr->ToString(false);
}
return TEXT("0.0.0.0");
}