259 lines
7.7 KiB
C++
259 lines
7.7 KiB
C++
#include "Chat.h"
|
|
|
|
#include <memory>
|
|
#include <unordered_set>
|
|
|
|
static void redisLogin(std::function<void(int)>&& callback,
|
|
const std::string& loginKey,
|
|
unsigned int timeout);
|
|
static void redisLogout(std::function<void(int)>&& callback,
|
|
const std::string& loginKey);
|
|
|
|
struct ClientContext
|
|
{
|
|
std::string name_;
|
|
std::string loginKey_;
|
|
std::string room_;
|
|
std::shared_ptr<nosql::RedisSubscriber> subscriber_;
|
|
};
|
|
|
|
static bool checkRoomNumber(const std::string& room)
|
|
{
|
|
if (room.empty() || room.size() > 2 || (room.size() == 2 && room[0] == '0'))
|
|
{
|
|
return false;
|
|
}
|
|
for (char c : room)
|
|
{
|
|
if (c < '0' || c > '9')
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Chat::handleNewMessage(const WebSocketConnectionPtr& wsConnPtr,
|
|
std::string&& message,
|
|
const WebSocketMessageType& type)
|
|
{
|
|
if (type == WebSocketMessageType::Close ||
|
|
type == WebSocketMessageType::Ping)
|
|
{
|
|
return;
|
|
}
|
|
if (type != WebSocketMessageType::Text &&
|
|
type != WebSocketMessageType::Pong)
|
|
{
|
|
LOG_ERROR << "Unsupported message type " << (int)type;
|
|
return;
|
|
}
|
|
LOG_DEBUG << "WsClient new message from "
|
|
<< wsConnPtr->peerAddr().toIpPort();
|
|
|
|
auto context = wsConnPtr->getContext<ClientContext>();
|
|
if (!context || context->name_.empty())
|
|
{
|
|
wsConnPtr->send("ERROR: You are not logged in.");
|
|
wsConnPtr->forceClose();
|
|
return;
|
|
}
|
|
|
|
auto redisClient = drogon::app().getRedisClient();
|
|
if (type == WebSocketMessageType::Pong)
|
|
{
|
|
redisClient->execCommandAsync(
|
|
[wsConnPtr](const nosql::RedisResult&) {
|
|
// Do nothing
|
|
},
|
|
[wsConnPtr](const nosql::RedisException& ex) {
|
|
LOG_ERROR << "Update user status failed: " << ex.what();
|
|
wsConnPtr->send("ERROR: Service unavailable.");
|
|
wsConnPtr->forceClose();
|
|
},
|
|
"SET %s 1 EX 120",
|
|
context->loginKey_.c_str());
|
|
return;
|
|
}
|
|
|
|
int operation = 0;
|
|
std::string room;
|
|
if (message.compare(0, 6, "ENTER ") == 0)
|
|
{
|
|
room = message.substr(6);
|
|
if (!checkRoomNumber(room))
|
|
{
|
|
wsConnPtr->send("ERROR: Invalid room number, should be [0-99].");
|
|
return;
|
|
}
|
|
operation = 1;
|
|
}
|
|
else if (message == "QUIT")
|
|
{
|
|
operation = 2;
|
|
}
|
|
|
|
switch (operation)
|
|
{
|
|
case 0: // Message
|
|
{
|
|
if (context->room_.empty())
|
|
{
|
|
wsConnPtr->send(
|
|
"ERROR: Not in room. Send 'ENTER roomNo' to enter a "
|
|
"room first.");
|
|
return;
|
|
}
|
|
// Publish message
|
|
std::string msg =
|
|
"[" + context->room_ + "][" + context->name_ + "] " + message;
|
|
|
|
// NOTICE: Dangerous to concat username into redis command!!!
|
|
// Do not use in production.
|
|
redisClient->execCommandAsync(
|
|
[](const nosql::RedisResult&) {},
|
|
[wsConnPtr](const nosql::RedisException& ex) {
|
|
wsConnPtr->send(std::string("ERROR: ") + ex.what());
|
|
},
|
|
"publish %s %s",
|
|
context->room_.c_str(),
|
|
msg.c_str());
|
|
break;
|
|
}
|
|
case 1: // Enter room
|
|
{
|
|
if (context->room_ == room)
|
|
{
|
|
wsConnPtr->send("ERROR: Already in room " + context->room_);
|
|
return;
|
|
}
|
|
if (!context->room_.empty())
|
|
{
|
|
context->subscriber_->unsubscribe(context->room_);
|
|
}
|
|
wsConnPtr->send("INFO: Enter room " + room);
|
|
context->subscriber_->subscribe(
|
|
room, [wsConnPtr](const std::string&, const std::string& msg) {
|
|
wsConnPtr->send(msg);
|
|
});
|
|
context->room_ = room;
|
|
break;
|
|
}
|
|
case 2: // Quit room
|
|
{
|
|
if (!context->room_.empty())
|
|
{
|
|
context->subscriber_->unsubscribe(context->room_);
|
|
wsConnPtr->send("INFO: Quit room " + context->room_);
|
|
context->room_.clear();
|
|
}
|
|
else
|
|
{
|
|
wsConnPtr->send(
|
|
"ERROR: Not in room. Send 'ENTER roomNo' to enter a "
|
|
"room first.");
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Chat::handleNewConnection(const HttpRequestPtr& req,
|
|
const WebSocketConnectionPtr& wsConnPtr)
|
|
{
|
|
LOG_DEBUG << "WsClient new connection from "
|
|
<< wsConnPtr->peerAddr().toIpPort();
|
|
const std::string name = req->getParameter("name");
|
|
if (name.empty())
|
|
{
|
|
wsConnPtr->send("Please give your name in parameters.");
|
|
wsConnPtr->forceClose();
|
|
}
|
|
|
|
std::string loginKey = "redis_chat:user:" + drogon::utils::getMd5(name);
|
|
redisLogin(
|
|
[wsConnPtr, name, loginKey](int status) {
|
|
if (status < 0)
|
|
{
|
|
wsConnPtr->send("ERROR: Service unavailable.");
|
|
wsConnPtr->shutdown();
|
|
return;
|
|
}
|
|
if (status == 0)
|
|
{
|
|
wsConnPtr->send("ERROR: User [" + name +
|
|
"] already logged in.");
|
|
wsConnPtr->shutdown();
|
|
return;
|
|
}
|
|
std::shared_ptr<ClientContext> context =
|
|
std::make_shared<ClientContext>();
|
|
context->subscriber_ =
|
|
drogon::app().getRedisClient()->newSubscriber();
|
|
context->name_ = name;
|
|
context->loginKey_ = loginKey;
|
|
wsConnPtr->setContext(context);
|
|
wsConnPtr->send("Hello, " + name + "!");
|
|
},
|
|
loginKey,
|
|
120);
|
|
}
|
|
|
|
void Chat::handleConnectionClosed(const WebSocketConnectionPtr& wsConnPtr)
|
|
{
|
|
LOG_DEBUG << "WsClient close connection from "
|
|
<< wsConnPtr->peerAddr().toIpPort();
|
|
auto context = wsConnPtr->getContext<ClientContext>();
|
|
// Channels will be auto unsubscribed when subscriber destructed.
|
|
// for (auto& channel : context->channels_)
|
|
// {
|
|
// context->subscriber_->unsubscribe(channel);
|
|
// }
|
|
if (context)
|
|
{
|
|
redisLogout([](int) {}, context->loginKey_);
|
|
}
|
|
}
|
|
|
|
static void redisLogin(std::function<void(int)>&& callback,
|
|
const std::string& loginKey,
|
|
unsigned int timeout)
|
|
{
|
|
static const char script[] = R"(
|
|
local exists = redis.call('GET', KEYS[1]);
|
|
if exists then return 0 end;
|
|
redis.call('SET', KEYS[1], 1, 'EX', ARGV[1]);
|
|
return 1;
|
|
)";
|
|
|
|
drogon::app().getRedisClient()->execCommandAsync(
|
|
[callback](const nosql::RedisResult& result) {
|
|
callback((int)result.asInteger());
|
|
},
|
|
[callback](const nosql::RedisException& ex) {
|
|
LOG_ERROR << "Login error: " << ex.what();
|
|
callback(-1);
|
|
},
|
|
"EVAL %s 1 %s %u",
|
|
script,
|
|
loginKey.c_str(),
|
|
timeout);
|
|
}
|
|
|
|
static void redisLogout(std::function<void(int)>&& callback,
|
|
const std::string& loginKey)
|
|
{
|
|
drogon::app().getRedisClient()->execCommandAsync(
|
|
[callback](const nosql::RedisResult& result) {
|
|
callback((int)result.asInteger());
|
|
},
|
|
[callback](const nosql::RedisException& ex) {
|
|
LOG_ERROR << "Logout error: " << ex.what();
|
|
callback(-1);
|
|
},
|
|
"DEL %s",
|
|
loginKey.c_str());
|
|
}
|