This commit is contained in:
Martin Slachta
2026-07-18 14:31:15 +02:00
commit a04f0dc262
3343 changed files with 1140208 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
add_subdirectory(chat_lib)
add_subdirectory(chat_service)
add_subdirectory(chat_client)
add_subdirectory(chat_server_exe)
@@ -0,0 +1,34 @@
project(tw_chat_client)
file(GLOB FILES
src/*.cpp
)
file(GLOB_RECURSE HEADERS
include/*.hpp
)
add_library(${PROJECT_NAME} OBJECT ${FILES} ${PROTO_FILES})
add_library(tw::chat_client ALIAS ${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PUBLIC FILE_SET HEADERS
BASE_DIRS include
FILES ${HEADERS}
)
target_include_directories(${PROJECT_NAME}
PUBLIC
./include/
)
target_link_libraries(${PROJECT_NAME}
PUBLIC
tw::network
tl::expected
tw::messaging
tw::protocol
protobuf::libprotobuf
tw::chat::lib
spdlog::spdlog
)
@@ -0,0 +1,37 @@
#pragma once
#include "MessageSession.hpp"
#include "SendChatMessage.hpp"
#include "ChatClientError.hpp"
#include "models/ChatMessage.hpp"
#include <functional>
#include <string>
namespace tw::chat {
class ChatClient {
tw::MessageSession m_session;
std::function<void(ChatMessage)> m_on_message;
std::function<void(tl::expected<void, ChatClientError>)> m_on_send_response;
std::function<void(uint32_t, tl::expected<void, ChatClientError>)> m_on_join_response;
std::function<void(uint32_t, tl::expected<void, ChatClientError>)> m_on_leave_response;
public:
ChatClient(const std::string& server_address, int16_t port);
void update();
tl::expected<void, ChatClientError> send_mesg(SendChatMessage message);
// Called for every broadcast message received on a subscribed channel.
void set_on_message(std::function<void(ChatMessage)> handler);
// Called when the server responds to send/join/leave requests.
// tl::expected holds void on success, ChatClientError on failure.
void set_on_send_response(std::function<void(tl::expected<void, ChatClientError>)> handler);
void set_on_join_response(std::function<void(uint32_t channel_id, tl::expected<void, ChatClientError>)> handler);
void set_on_leave_response(std::function<void(uint32_t channel_id, tl::expected<void, ChatClientError>)> handler);
};
}
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <string>
namespace tw::chat {
struct SendChatMessage {
uint64_t channel_id;
std::string message;
};
}
@@ -0,0 +1,97 @@
#include "ChatClient.hpp"
#include "Chat.pb.h"
#include "MessageRegistry.hpp"
#include "SendChatMessage.hpp"
namespace {
tl::expected<void, tw::chat::ChatClientError> from_error_code(mmo::chat::ChatErrorCode code) {
switch (code) {
case mmo::chat::CHAT_ERROR_CODE_OK:
return {};
case mmo::chat::CHAT_ERROR_CODE_CHANNEL_NOT_FOUND:
case mmo::chat::CHAT_ERROR_CODE_ALREADY_IN_CHANNEL:
case mmo::chat::CHAT_ERROR_CODE_NOT_IN_CHANNEL:
return tl::make_unexpected(tw::chat::ChatClientError::ChannelNotFound);
default:
return tl::make_unexpected(tw::chat::ChatClientError::PermissionDenied);
}
}
} // namespace
namespace tw::chat {
ChatClient::ChatClient(const std::string& server_address, int16_t port)
: m_session(net::Address(server_address, port))
{
m_session.set_handler(CHAT_MESSAGE_BROADCAST_REQUEST, [this](std::span<const std::byte> data) {
if (!m_on_message) return;
mmo::chat::ChatMessageBroadcastRequest proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
ChatMessage msg;
msg.channel_id = proto.channel_id();
msg.client_id = proto.sender_id();
msg.message = proto.message();
msg.timestamp = ChatMessage::Clock::now();
m_on_message(std::move(msg));
});
m_session.set_handler(CHAT_SEND_MESSAGE_RESPONSE, [this](std::span<const std::byte> data) {
if (!m_on_send_response) return;
mmo::chat::SendChatMessageResponse proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
m_on_send_response(from_error_code(proto.error()));
});
m_session.set_handler(CHAT_JOIN_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
if (!m_on_join_response) return;
mmo::chat::JoinChannelResponse proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
m_on_join_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
});
m_session.set_handler(CHAT_LEAVE_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
if (!m_on_leave_response) return;
mmo::chat::LeaveChannelResponse proto;
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
m_on_leave_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
});
}
void ChatClient::update() {
m_session.update();
}
tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage message) {
mmo::chat::SendChatMessageRequest mesg;
mesg.set_channel_id(message.channel_id);
mesg.set_message(message.message);
std::vector<std::byte> buf(mesg.ByteSizeLong());
(void)mesg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
auto send_r = m_session.send(Message<mmo::chat::SendChatMessageRequest>::value,
std::span(buf), true);
if (!send_r)
return tl::make_unexpected(ChatClientError::PermissionDenied);
return {};
}
void ChatClient::set_on_message(std::function<void(ChatMessage)> handler) {
m_on_message = std::move(handler);
}
void ChatClient::set_on_send_response(std::function<void(tl::expected<void, ChatClientError>)> handler) {
m_on_send_response = std::move(handler);
}
void ChatClient::set_on_join_response(std::function<void(uint32_t, tl::expected<void, ChatClientError>)> handler) {
m_on_join_response = std::move(handler);
}
void ChatClient::set_on_leave_response(std::function<void(uint32_t, tl::expected<void, ChatClientError>)> handler) {
m_on_leave_response = std::move(handler);
}
} // namespace tw::chat
@@ -0,0 +1,19 @@
project(tw_chat_lib)
file(GLOB_RECURSE HEADERS
include/*.hpp
)
add_library(${PROJECT_NAME} OBJECT ${HEADERS} ${PROTO_FILES})
add_library(tw::chat::lib ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME}
PUBLIC
./include/
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
spdlog::spdlog
tw::network
)
@@ -0,0 +1,10 @@
#pragma once
namespace tw::chat {
enum class ChatClientError {
PermissionDenied,
ChannelNotFound,
};
} // namespace tw::chat
@@ -0,0 +1,10 @@
#pragma once
namespace tw::chat {
enum class ChatServerError {
PermissionDenied,
ChannelNotFound,
};
} // namespace tw::chat
@@ -0,0 +1,20 @@
#pragma once
#include <chrono>
namespace tw::chat {
class ChatMessage {
public:
using Clock = std::chrono::system_clock;
using TimePoint = Clock::time_point;
TimePoint timestamp;
uint64_t client_id;
uint64_t channel_id;
std::string message;
};
}
@@ -0,0 +1,20 @@
project(tw_chat_server_exe)
add_executable(${PROJECT_NAME}
src/ChatServerController.cpp
src/ChatServerRuntime.cpp
)
target_include_directories(${PROJECT_NAME}
PRIVATE
src/
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
tw::chat::service
tw::messaging
tw::protocol
tw::network
spdlog::spdlog
)
@@ -0,0 +1,123 @@
#include "ChatServerController.hpp"
#include "Chat.pb.h"
#include "MessageRegistry.hpp"
#include <spdlog/spdlog.h>
#include <cstring>
namespace tw::chat {
static mmo::chat::ChatErrorCode to_error_code(tl::expected<void, ChatServerError> result) {
if (result) return mmo::chat::CHAT_ERROR_CODE_OK;
switch (result.error()) {
case ChatServerError::PermissionDenied: return mmo::chat::CHAT_ERROR_CODE_NOT_IN_CHANNEL;
case ChatServerError::ChannelNotFound: return mmo::chat::CHAT_ERROR_CODE_CHANNEL_NOT_FOUND;
}
return mmo::chat::CHAT_ERROR_CODE_CHANNEL_NOT_FOUND;
}
template<typename T>
static std::vector<std::byte> serialize(const T& msg) {
std::vector<std::byte> buf(msg.ByteSizeLong());
(void)msg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
return buf;
}
ChatServerController::ChatServerController(int port)
: m_endpoint(net::quicr::QuicrEndpoint::create_and_bind(port).value())
, m_listener(net::quicr::QuicrConnectionListener::listen(m_endpoint.get()).value())
, m_service([this](uint64_t id, const ChatMessage& msg) { broadcast(id, msg); })
{
register_handlers();
spdlog::info("Chat server listening on port {}", port);
}
void ChatServerController::register_handlers() {
m_handlers[Message<mmo::chat::SendChatMessageRequest>::value] =
[this](uint64_t client_id, std::span<const std::byte> data) {
mmo::chat::SendChatMessageRequest msg;
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
mmo::chat::SendChatMessageResponse r;
r.set_channel_id(msg.channel_id());
r.set_error(to_error_code(m_service.send_message(client_id, msg.channel_id(), msg.message())));
send_to(client_id, Message<mmo::chat::SendChatMessageResponse>::value, serialize(r));
};
m_handlers[Message<mmo::chat::JoinChannelRequest>::value] =
[this](uint64_t client_id, std::span<const std::byte> data) {
mmo::chat::JoinChannelRequest msg;
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
m_service.join_channel(client_id, msg.channel_id());
mmo::chat::JoinChannelResponse r;
r.set_channel_id(msg.channel_id());
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
send_to(client_id, Message<mmo::chat::JoinChannelResponse>::value, serialize(r));
};
m_handlers[Message<mmo::chat::LeaveChannelRequest>::value] =
[this](uint64_t client_id, std::span<const std::byte> data) {
mmo::chat::LeaveChannelRequest msg;
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
m_service.leave_channel(client_id, msg.channel_id());
mmo::chat::LeaveChannelResponse r;
r.set_channel_id(msg.channel_id());
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
send_to(client_id, Message<mmo::chat::LeaveChannelResponse>::value, serialize(r));
};
}
void ChatServerController::update() {
m_endpoint->poll();
net::quicr::QuicrConnection* conn = nullptr;
while ((conn = m_listener->listen())) {
m_connections.emplace(conn->self_id(), conn);
spdlog::info("Chat client connected: {}", conn->self_id());
}
for (auto& [client_id, conn] : m_connections) {
auto r = conn->read_into(m_recv_buf);
if (!r || *r == 0) continue;
dispatch(client_id, std::span(m_recv_buf.data(), *r));
}
}
void ChatServerController::dispatch(uint64_t client_id, std::span<const std::byte> data) {
constexpr size_t HEADER = sizeof(uint32_t) * 2;
if (data.size() < HEADER) {
spdlog::warn("ChatServerController: dropped short datagram ({} bytes)", data.size());
return;
}
uint32_t type{};
std::memcpy(&type, data.data(), sizeof(type));
if (type >= m_handlers.size() || !m_handlers[type]) {
spdlog::warn("ChatServerController: no handler for type {}", type);
return;
}
m_handlers[type](client_id, data.subspan(HEADER));
}
void ChatServerController::send_to(uint64_t client_id, uint32_t type,
std::span<const std::byte> payload, bool reliable) {
auto it = m_connections.find(client_id);
if (it == m_connections.end()) return;
constexpr uint32_t SEQ_NONE = 0;
std::vector<std::byte> buf(sizeof(type) + sizeof(SEQ_NONE) + payload.size());
std::memcpy(buf.data(), &type, sizeof(type));
std::memcpy(buf.data() + sizeof(type), &SEQ_NONE, sizeof(SEQ_NONE));
std::memcpy(buf.data() + sizeof(type) + sizeof(SEQ_NONE), payload.data(), payload.size());
(void)it->second->send_message(std::span(buf), reliable);
}
void ChatServerController::broadcast(uint64_t client_id, const ChatMessage& msg) {
mmo::chat::ChatMessageBroadcastRequest bcast;
bcast.set_channel_id(msg.channel_id);
bcast.set_sender_id(msg.client_id);
bcast.set_message(msg.message);
send_to(client_id, Message<mmo::chat::ChatMessageBroadcastRequest>::value,
serialize(bcast), true);
}
} // namespace tw::chat
@@ -0,0 +1,40 @@
#pragma once
#include "ChatService.hpp"
#include "protocol/quicr/QuicrEndpoint.hpp"
#include "protocol/quicr/QuicrConnectionListener.hpp"
#include <array>
#include <cstdint>
#include <functional>
#include <span>
#include <unordered_map>
#include <vector>
namespace tw::chat {
class ChatServerController {
static constexpr size_t MAX_TYPES = 32;
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
std::unique_ptr<net::quicr::QuicrConnectionListener> m_listener;
std::unordered_map<uint64_t, net::quicr::QuicrConnection*> m_connections;
std::vector<std::byte> m_recv_buf{64 * 1024};
ChatService m_service;
std::array<std::function<void(uint64_t, std::span<const std::byte>)>, MAX_TYPES> m_handlers{};
public:
explicit ChatServerController(int port = CHAT_DEFAULT_PORT);
void update();
private:
void register_handlers();
void dispatch(uint64_t client_id, std::span<const std::byte> data);
void send_to(uint64_t client_id, uint32_t type, std::span<const std::byte> payload,
bool reliable = false);
void broadcast(uint64_t client_id, const ChatMessage& msg);
};
} // namespace tw::chat
@@ -0,0 +1,37 @@
#include "ChatServerController.hpp"
#include <spdlog/spdlog.h>
#include <atomic>
#include <csignal>
#include <thread>
#include <chrono>
static std::atomic<bool> g_quit{false};
static void on_signal(int) {
g_quit.store(true);
}
static void register_signal_handler() {
struct sigaction sa{};
sa.sa_handler = on_signal;
sigfillset(&sa.sa_mask);
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
}
int main() {
register_signal_handler();
spdlog::info("Starting chat server on port {}", tw::chat::CHAT_DEFAULT_PORT);
tw::chat::ChatServerController controller;
while (!g_quit.load()) {
controller.update();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
spdlog::info("Chat server shutting down");
return 0;
}
@@ -0,0 +1,32 @@
project(tw_chat_service)
file(GLOB FILES
src/*.cpp
)
file(GLOB_RECURSE HEADERS
include/*.hpp
)
add_library(${PROJECT_NAME} OBJECT ${FILES})
add_library(tw::chat::service ALIAS ${PROJECT_NAME})
target_sources(${PROJECT_NAME}
PUBLIC FILE_SET HEADERS
BASE_DIRS include
FILES ${HEADERS}
)
target_include_directories(${PROJECT_NAME}
PUBLIC
./include/
)
target_link_libraries(${PROJECT_NAME}
PUBLIC
tw::chat::lib
tl::expected
spdlog::spdlog
)
add_subdirectory(tests)
@@ -0,0 +1,33 @@
#pragma once
#include "services/ChatChannelManager.hpp"
#include "services/ChatChannelSubscriptionManager.hpp"
#include "services/ChatMessagePropagator.hpp"
#include <tl/expected.hpp>
#include <cstdint>
#include <functional>
#include <string>
namespace tw::chat {
const int16_t CHAT_DEFAULT_PORT = 8099;
class ChatService {
ChatChannelSubscriptionManager m_subscriptions;
ChatMessagePropagator m_propagator;
ChatChannelManager m_channel_manager;
public:
explicit ChatService(std::function<void(uint64_t, const ChatMessage&)> broadcast_fn);
void add_channel(ChannelId id, ChatChannel channel);
tl::expected<void, ChatServerError> send_message(uint64_t client_id,
uint64_t channel_id,
const std::string& message);
void join_channel(uint64_t client_id, uint64_t channel_id);
void leave_channel(uint64_t client_id, uint64_t channel_id);
};
} // namespace tw::chat
@@ -0,0 +1,8 @@
#pragma once
#include <cstdint>
namespace tw::chat {
using ChannelId = uint64_t;
}
@@ -0,0 +1,24 @@
#pragma once
#include "models/ChatMessage.hpp"
#include <cstdint>
#include <string>
#include <vector>
namespace tw::chat {
class ChatChannel {
uint64_t id;
std::string name;
std::vector<ChatMessage> messages;
public:
ChatChannel(uint64_t id, std::string name) : id(id), name(std::move(name)) {}
bool add_message(const ChatMessage& message) {
messages.push_back(message);
return true;
}
};
}
@@ -0,0 +1,29 @@
#pragma once
#include "services/ChatMessagePropagator.hpp"
#include "models/ChannelId.hpp"
#include "models/ChatChannel.hpp"
#include "ChatServerError.hpp"
#include <tl/expected.hpp>
#include <string>
#include <unordered_map>
namespace tw::chat {
class ChatChannelManager {
std::unordered_map<ChannelId, ChatChannel> m_channels;
ChatMessagePropagator* m_propagator;
public:
explicit ChatChannelManager(ChatMessagePropagator* propagator);
bool has_channel(ChannelId channel_id) const;
ChatChannel& get_channel(ChannelId channel_id);
void add_channel(ChannelId channel_id, ChatChannel channel);
void remove_channel(ChannelId channel_id);
bool can_client_write(ChannelId channel_id, uint64_t client_id) const;
tl::expected<void, ChatServerError> add_message(uint64_t client_id, uint64_t channel_id, const std::string& message);
};
} // namespace tw::chat
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace tw::chat {
class ChatChannelSubscriptionManager {
std::unordered_map<uint64_t, std::vector<uint64_t>> m_client_channels;
std::unordered_map<uint64_t, std::vector<uint64_t>> m_channel_clients;
public:
void add_subscription(uint64_t client_id, uint64_t channel_id);
void remove_subscription(uint64_t client_id, uint64_t channel_id);
void get_subscriptions(uint64_t client_id, std::vector<uint64_t>& out) const;
const std::vector<uint64_t>& get_subscribers(uint64_t channel_id) const;
};
} // namespace tw::chat
@@ -0,0 +1,24 @@
#pragma once
#include "services/ChatChannelSubscriptionManager.hpp"
#include "models/ChatMessage.hpp"
#include <cstdint>
#include <functional>
namespace tw::chat {
class ChatMessagePropagator {
const ChatChannelSubscriptionManager* m_subscriptions;
std::function<void(uint64_t client_id, const ChatMessage&)> m_send;
public:
ChatMessagePropagator(
const ChatChannelSubscriptionManager* subscriptions,
std::function<void(uint64_t, const ChatMessage&)> send
);
void on_message_sent(const ChatMessage& message);
};
} // namespace tw::chat
@@ -0,0 +1,37 @@
#include "services/ChatChannelManager.hpp"
namespace tw::chat {
ChatChannelManager::ChatChannelManager(ChatMessagePropagator* propagator)
: m_propagator(propagator) {}
bool ChatChannelManager::has_channel(ChannelId channel_id) const {
return m_channels.contains(channel_id);
}
ChatChannel& ChatChannelManager::get_channel(ChannelId channel_id) {
return m_channels.at(channel_id);
}
void ChatChannelManager::add_channel(ChannelId channel_id, ChatChannel channel) {
m_channels.emplace(channel_id, std::move(channel));
}
void ChatChannelManager::remove_channel(ChannelId channel_id) {
m_channels.erase(channel_id);
}
bool ChatChannelManager::can_client_write(ChannelId channel_id, uint64_t) const {
return has_channel(channel_id);
}
tl::expected<void, ChatServerError> ChatChannelManager::add_message(uint64_t client_id, uint64_t channel_id, const std::string& message) {
if (!has_channel(channel_id))
return tl::unexpected(ChatServerError::ChannelNotFound);
ChatMessage msg{ChatMessage::Clock::now(), client_id, channel_id, message};
get_channel(channel_id).add_message(msg);
m_propagator->on_message_sent(msg);
return {};
}
} // namespace tw::chat
@@ -0,0 +1,26 @@
#include "services/ChatChannelSubscriptionManager.hpp"
namespace tw::chat {
void ChatChannelSubscriptionManager::add_subscription(uint64_t client_id, uint64_t channel_id) {
m_client_channels[client_id].push_back(channel_id);
m_channel_clients[channel_id].push_back(client_id);
}
void ChatChannelSubscriptionManager::remove_subscription(uint64_t client_id, uint64_t channel_id) {
std::erase(m_client_channels[client_id], channel_id);
std::erase(m_channel_clients[channel_id], client_id);
}
void ChatChannelSubscriptionManager::get_subscriptions(uint64_t client_id, std::vector<uint64_t>& out) const {
if (auto it = m_client_channels.find(client_id); it != m_client_channels.end())
out = it->second;
}
const std::vector<uint64_t>& ChatChannelSubscriptionManager::get_subscribers(uint64_t channel_id) const {
static const std::vector<uint64_t> empty;
auto it = m_channel_clients.find(channel_id);
return it != m_channel_clients.end() ? it->second : empty;
}
} // namespace tw::chat
@@ -0,0 +1,18 @@
#include "services/ChatMessagePropagator.hpp"
namespace tw::chat {
ChatMessagePropagator::ChatMessagePropagator(
const ChatChannelSubscriptionManager* subscriptions,
std::function<void(uint64_t, const ChatMessage&)> send
) :
m_subscriptions(subscriptions),
m_send(std::move(send))
{}
void ChatMessagePropagator::on_message_sent(const ChatMessage& message) {
for (uint64_t client_id : m_subscriptions->get_subscribers(message.channel_id))
m_send(client_id, message);
}
} // namespace tw::chat
@@ -0,0 +1,37 @@
#include "ChatService.hpp"
#include <algorithm>
#include <spdlog/spdlog.h>
namespace tw::chat {
ChatService::ChatService(std::function<void(uint64_t, const ChatMessage&)> broadcast_fn)
: m_propagator(&m_subscriptions, std::move(broadcast_fn))
, m_channel_manager(&m_propagator)
{
m_channel_manager.add_channel(1, ChatChannel(1, "Test"));
}
void ChatService::add_channel(ChannelId id, ChatChannel channel) {
m_channel_manager.add_channel(id, std::move(channel));
}
tl::expected<void, ChatServerError> ChatService::send_message(
uint64_t client_id, uint64_t channel_id, const std::string& message)
{
const auto& subscribers = m_subscriptions.get_subscribers(channel_id);
const bool is_subscribed = std::ranges::find(subscribers, client_id) != subscribers.end();
if (!is_subscribed)
return tl::unexpected(ChatServerError::PermissionDenied);
return m_channel_manager.add_message(client_id, channel_id, message);
}
void ChatService::join_channel(uint64_t client_id, uint64_t channel_id) {
m_subscriptions.add_subscription(client_id, channel_id);
}
void ChatService::leave_channel(uint64_t client_id, uint64_t channel_id) {
m_subscriptions.remove_subscription(client_id, channel_id);
}
} // namespace tw::chat
@@ -0,0 +1 @@
add_subdirectory(chat_mock_client)
@@ -0,0 +1,11 @@
project(tw_chat_mock_client)
add_executable(${PROJECT_NAME} ChatMockClient.cpp)
target_link_libraries(${PROJECT_NAME}
PRIVATE
tw::protocol
tw::messaging
tw::network
spdlog::spdlog
)
@@ -0,0 +1,120 @@
#include "Address.hpp"
#include "MessageSession.hpp"
#include "MessageRegistry.hpp"
#include "Chat.pb.h"
#include <spdlog/spdlog.h>
#include <atomic>
#include <chrono>
#include <csignal>
#include <print>
#include <string>
#include <sys/select.h>
#include <thread>
#include <unistd.h>
#include <vector>
static std::atomic<bool> g_quit{false};
static void on_signal(int) { g_quit.store(true); }
static void register_signal_handler() {
struct sigaction sa{};
sa.sa_handler = on_signal;
sigfillset(&sa.sa_mask);
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
}
template<typename T>
static std::vector<std::byte> serialize(const T& msg) {
std::vector<std::byte> buf(msg.ByteSizeLong());
(void)msg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
return buf;
}
int main(int argc, char* argv[]) {
register_signal_handler();
std::string host = "127.0.0.1";
int port = 8099;
uint64_t channel_id = 0;
bool channel_set = false;
for (int i = 1; i < argc; ++i) {
std::string_view arg(argv[i]);
if (arg == "--host" && i + 1 < argc) host = argv[++i];
else if (arg == "--port" && i + 1 < argc) port = std::stoi(argv[++i]);
else if (arg == "--channel" && i + 1 < argc) {
channel_id = std::stoull(argv[++i]);
channel_set = true;
}
}
if (!channel_set) {
std::println(stderr, "Usage: {} [--host <ip>] [--port <port>] --channel <id>", argv[0]);
return 1;
}
tw::MessageSession session(tw::net::Address{std::string{host}, port});
spdlog::info("Connecting to {}:{}...", host, port);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (!session.is_established()) {
if (std::chrono::steady_clock::now() > deadline) {
spdlog::error("Connection timed out");
return 1;
}
session.update();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
spdlog::info("Connected. Joining channel {}...", channel_id);
session.set_handler(tw::Message<mmo::chat::ChatMessageBroadcastRequest>::value,
[](std::span<const std::byte> data) {
mmo::chat::ChatMessageBroadcastRequest bcast;
bcast.ParseFromArray(data.data(), static_cast<int>(data.size()));
std::println("[ch:{}] <{}> {}", bcast.channel_id(), bcast.sender_id(), bcast.message());
});
mmo::chat::JoinChannelRequest join;
join.set_channel_id(channel_id);
(void)session.request(
tw::Message<mmo::chat::JoinChannelRequest>::value,
serialize(join),
[channel_id](std::span<const std::byte> data) {
mmo::chat::JoinChannelResponse resp;
resp.ParseFromArray(data.data(), static_cast<int>(data.size()));
std::println("Joined channel {} successfully.", channel_id);
});
std::println("Joined channel {}. Type a message and press Enter. Ctrl+C to quit.", channel_id);
while (!g_quit.load()) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
struct timeval tv{0, 10'000};
if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) > 0) {
std::string line;
if (!std::getline(std::cin, line)) break;
if (!line.empty()) {
mmo::chat::SendChatMessageRequest msg;
msg.set_channel_id(channel_id);
msg.set_message(line);
(void)session.request(
tw::Message<mmo::chat::SendChatMessageRequest>::value,
serialize(msg),
[channel_id](std::span<const std::byte> data) {
mmo::chat::SendChatMessageResponse resp;
resp.ParseFromArray(data.data(), static_cast<int>(data.size()));
std::println("Message sent to channel {}.", channel_id);
});
}
}
session.update();
}
return 0;
}