1
0
Fork 0
mirror of https://gitlab.com/niansa/pyChat.git synced 2025-03-06 20:53:34 +01:00
pyChat/main.cpp
2022-04-09 23:20:00 +02:00

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;
}
}