mirror of
https://gitlab.com/niansa/pyChat.git
synced 2025-03-06 20:53:34 +01:00
193 lines
6.5 KiB
C++
193 lines
6.5 KiB
C++
#define BOOST_ASIO_HAS_CO_AWAIT
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <exception>
|
|
#include <stdexcept>
|
|
#include <memory>
|
|
#include <boost/asio.hpp>
|
|
|
|
using namespace boost;
|
|
|
|
|
|
|
|
std::string_view streambufAsString(asio::streambuf& buffer) {
|
|
return std::string_view{reinterpret_cast<const char*>(buffer.data().data()), buffer.data().size()};
|
|
}
|
|
|
|
bool isPrintable(std::string_view str) {
|
|
for (const auto c : str) {
|
|
if (!isprint(c)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
enum class Event {
|
|
join, leave, message
|
|
};
|
|
|
|
|
|
class User {
|
|
std::vector<std::shared_ptr<User>>& users;
|
|
asio::ip::tcp::socket socket;
|
|
std::string nickname;
|
|
bool good = true;
|
|
|
|
public:
|
|
User(asio::io_context& ioc, std::vector<std::shared_ptr<User>>& users) : socket(ioc), users(users) {}
|
|
|
|
auto& getSocket() {
|
|
return socket;
|
|
}
|
|
const auto& getNickname() const {
|
|
return nickname;
|
|
}
|
|
bool isGood() const {
|
|
return good;
|
|
}
|
|
bool isLoggedOn() const {
|
|
return good && !nickname.empty();
|
|
}
|
|
bool wasLoggedOn() {
|
|
return !nickname.empty();
|
|
}
|
|
void markAsBad() {
|
|
good = false;
|
|
}
|
|
|
|
asio::awaitable<void> sendString(std::string_view str) {
|
|
co_await asio::async_write(socket, asio::buffer(str), asio::use_awaitable);
|
|
}
|
|
asio::awaitable<std::string> recvLine() {
|
|
asio::streambuf buffer;
|
|
co_await asio::async_read_until(socket, buffer, '\n', asio::use_awaitable);
|
|
auto fres = std::string(streambufAsString(buffer));
|
|
fres.pop_back();
|
|
co_return fres;
|
|
}
|
|
|
|
asio::awaitable<void> connect() {
|
|
try {
|
|
// Ask for nickname
|
|
co_await sendString("Welcome to pyChat! Please type your nickname: ");
|
|
while (true) {
|
|
// Get nickname as string
|
|
auto proposedNickname = co_await recvLine();
|
|
// Check that nickname has a healthy length
|
|
if (proposedNickname.empty() || proposedNickname.size() > 30) {
|
|
co_await sendString("Your nickname has a bad length. Please choose a different one: ");
|
|
continue;
|
|
}
|
|
// Check that nickname is printable
|
|
if (!isPrintable(proposedNickname)) {
|
|
co_await sendString("Your nickname is invalid. Please choose a different one: ");
|
|
continue;
|
|
}
|
|
// Check that nickname is not already taken
|
|
if (std::find_if(users.begin(), users.end(), [&, this] (const auto& o) {return o->getNickname() == proposedNickname;}) != users.end()) {
|
|
co_await sendString("This nickname has already been taken. Please choose a different one: ");
|
|
continue;
|
|
}
|
|
// Use this nickname
|
|
nickname = std::move(proposedNickname);
|
|
break;
|
|
}
|
|
std::cout << "User has connected as " << nickname << std::endl;
|
|
// Broadcast join
|
|
co_await broadcastEvent(Event::join);
|
|
// Wait loop for messages
|
|
while (true) {
|
|
auto message = co_await recvLine();
|
|
// Check kind of message
|
|
if (message.starts_with('/')) { // Command
|
|
if (message == "/quit") {
|
|
markAsBad();
|
|
throw std::runtime_error("User initiated disconnect");
|
|
} else {
|
|
co_await sendString("Invalid command: "+message+'\n');
|
|
}
|
|
} else { // Message
|
|
// Make sure message is printable
|
|
if (!isPrintable(message)) {
|
|
co_await sendString("Message could not be sent because it contains non-printable characters.\n");
|
|
continue;
|
|
}
|
|
// Broadcast message
|
|
co_await broadcastEvent(Event::message, message);
|
|
}
|
|
}
|
|
} catch (std::exception& e) {
|
|
std::cout << "User " << (nickname.empty() ? "<unknown>" : nickname) << " has disconnected: " << e.what() << std::endl;
|
|
markAsBad();
|
|
}
|
|
// Broadcast user disconnect
|
|
if (wasLoggedOn()) {
|
|
co_await broadcastEvent(Event::leave);
|
|
}
|
|
// Delete user from list
|
|
auto it = std::find_if(users.begin(), users.end(), [this] (const auto& o) {
|
|
return o.get() == this;
|
|
});
|
|
users.erase(it);
|
|
}
|
|
|
|
asio::awaitable<void> onEvent(User& sender, Event event, std::string_view param = "") {
|
|
switch (event) {
|
|
case Event::join: co_await sendString("* "+sender.nickname+" has joined.\n"); break;
|
|
case Event::leave: co_await sendString("* "+sender.nickname+" has left.\n"); break;
|
|
case Event::message: co_await sendString(" <"+sender.nickname+"> "+std::string(param)+'\n'); break;
|
|
}
|
|
}
|
|
|
|
asio::awaitable<void> broadcastEvent(Event event, std::string_view param = "") {
|
|
for (auto& user : users) {
|
|
if (user->isLoggedOn()) {
|
|
co_await user->onEvent(*this, event, param);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
class Server {
|
|
asio::io_context& ioc;
|
|
std::vector<std::shared_ptr<User>> users;
|
|
|
|
public:
|
|
Server(asio::io_context& ioc) : ioc(ioc) {}
|
|
|
|
asio::awaitable<void> listen(int port) {
|
|
try {
|
|
// Create acceptor
|
|
asio::ip::tcp::acceptor acceptor(ioc, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port));
|
|
// Handle all incoming connections
|
|
while (true) {
|
|
auto user = std::make_shared<User>(ioc, users);
|
|
co_await acceptor.async_accept(user->getSocket(), asio::use_awaitable);
|
|
asio::co_spawn(ioc, user->connect(), asio::detached);
|
|
users.push_back(std::move(user));
|
|
}
|
|
} catch (std::exception& e) {
|
|
std::cerr << "Failed to stay alive: " << e.what() << std::endl;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
int main() {
|
|
try {
|
|
asio::io_context ioc;
|
|
Server server(ioc);
|
|
int port = 22080;
|
|
asio::co_spawn(ioc, server.listen(port), asio::detached);
|
|
std::cout << "Server has initialized and will now be listening on port " << port << '.' << std::endl;
|
|
ioc.run();
|
|
} catch (std::exception& e) {
|
|
std::cerr << "Failed to launch: " << e.what() << std::endl;
|
|
}
|
|
}
|