/* * asbots * Copyright (C) 2021 niansa * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "instance.hpp" #include "config.hpp" #include "uid.hpp" #include "utility.hpp" #include #include #include #include #include #include #include using fmt::operator""_format; using std::literals::operator""sv; using namespace Utility; void Event::parse(std::string_view str) { auto split = strSplit(str, ' ', 2); // Check split size argsSizeCheck("basic", split, 2); // Move values split[0] = {split[0].data()+1, split[0].size()-1}; // Erase leading ':' auto id_len = split[0].size(); if (id_len == SUID_len) { sender = SUID(std::move(split[0])); } else if (id_len == UUID_len) { sender = UUID(std::move(split[0])); } else { sender = split[0]; } name = std::move(split[1]); if (split.size() == 3) { // Get args and text std::tie(raw_args, text) = splitOnce(split[2], " :"); } } void Command::parse(std::string_view str) { auto split = strSplit(str, ' ', 1); name = std::move(split[0]); // Get text from args std::tie(args, text) = splitOnce(split[1], " :"); } template void ModeSet::parse(std::string_view in, NetworkInfo &netInfo) { enum { ADDING, REMOVING } op; // Split up auto split = strSplit(in, ' '); auto paramIt = split.begin() + 1; // Iterate through mode characters for (const char character : split[0]) { if (character == '+') { op = ADDING; } else if (character == '-') { op = REMOVING; } else { if (op == ADDING) { if (str.find(character) == str.npos || netInfo.channelModes.isListMode(character)) { str.push_back(character); if constexpr(channelModes) { if (netInfo.channelModes.takesParamOnSet(character)) { if (paramIt == split.end()) { throw DesyncError(); } params.push_back({character, std::string(*(paramIt++))}); } } } else { throw DesyncError(); } } else if (op == REMOVING) { auto res = str.find(character); if (res != str.npos) { str.erase(res, 1); if constexpr(channelModes) { if (netInfo.channelModes.takesParamOnUnset(character)) { if (paramIt == split.end()) { throw DesyncError(); } for (auto it = params.begin(); ; it++) { if (it == params.end()) { throw DesyncError(); } auto &[c, d] = *it; if (c == character && (d == *paramIt || *paramIt == "*")) { params.erase(it); break; } } paramIt++; } } } else { throw DesyncError(); } } } } } void User::parse_euid(const Event& event, NetworkInfo& netInfo) { this->server = std::get(event.sender.id); // Check size argsSizeCheck("EUID", event.args, 10); // Move values nick = event.args[0]; hops = std::stoull(std::string(event.args[1])); umode.parse(event.args[3], netInfo); ident = event.args[4]; vhost = event.args[5]; ip = event.args[6]; uid = UUID(event.args[7]); realhost = event.args[8]; if (event.args[9] != "*" && event.args[9] != "0") { loginName = event.args[9]; } // Get realname realname = std::move(event.text); } void User::removeChannel(const Channel *channel) { std::remove_if(channels.begin(), channels.end(), [&] (auto obj) {return obj == channel;}); } void Channel::parse_sjoin(const Event& event, Cache& cache, NetworkInfo& netInfo) { this->server = std::get(event.sender.id); // Check size argsSizeCheck("SJOIN", event.args, 3); // Set values creationTime = std::stoull(std::string(event.args[0])); name = event.args[1]; mode.parse(event.args[2], netInfo); // Get members for (auto& raw_uuid : strSplit(event.text, ' ')) { // Erase prefix while (raw_uuid.size() > UUID_len) { char prefixChar = raw_uuid[0]; raw_uuid = {raw_uuid.data()+1, raw_uuid.size()-1}; char modeChar = netInfo.channelModes.prefixMap[prefixChar]; mode.str.push_back(modeChar); mode.params.push_back({modeChar, std::string(raw_uuid)}); } // Find user in cache auto res = cache.find_user_by_uid(UUID(raw_uuid)); if (res == cache.users.end()) { throw DesyncError(); } User *member = res->get(); // Append member to list members.push_back(member); } } void Channel::removeMember(const User *member) { std::remove_if(members.begin(), members.end(), [&] (auto obj) {return obj == member;}); } std::vector>::iterator Cache::find_user_by_nick(std::string_view nick) { for (auto it = users.begin(); ; it++) { if (it == users.end() || it->get()->nick == nick) { return it; } } } std::vector>::iterator Cache::find_user_by_uid(const UUID& uid) { for (auto it = users.begin(); ; it++) { if (it == users.end() || it->get()->uid == uid) { return it; } } } std::vector>::iterator Cache::find_channel_by_name(std::string_view name) { for (auto it = channels.begin(); ; it++) { if (it == channels.end() || it->get()->name == name) { return it; } } } async::result Instance::run() { // Prepare services for (auto& service : services) { service->i = this; service->uuid = UUIDGen(); async::detach(service->intitialize()); } // Create connection socket.reset(new uvpp::tcp{s}); co_await socket->connect(addr); socket->recv_start(); // Login co_await login(); // Mainloop while (true) { // Read auto data = co_await socket->recv(); // Check for general error if (data->error()) { continue; } // Check for broken connection if (data->broken()) { break; } // Make string auto dataStr = std::string_view{ data->data.get(), static_cast(data->nread) }; // Split by newlines for (auto& line : strSplit(dataStr, '\n')) { if (line.size() < 2) { continue; // Empty line } // Remove \r if (line.back() == '\r') { line = {line.data(), line.size()-1}; } // Check if server sent an event or command, then parse and process it if (line[0] == ':') { Event event; event.parse(std::move(line)); async::detach(process(std::move(event))); } else { Command command; command.parse(std::move(line)); async::detach(process(std::move(command))); } } } } async::result Instance::login() { co_await socket->send("PASS {} TS 6 :{}\n"_format(config.auth.send_password, config.server.uid.str())); co_await socket->send("CAPAB :QS EX IE KLN UNKLN ENCAP TB SERVICES EUID EOPMOD MLOCK\n"sv); co_await socket->send("SERVER {} 1 :{}{}\n"_format(config.server.name, config.server.hidden ? "(H) " : "", config.server.description)); co_await socket->send("SVINFO 6 3 0 :{}\n"_format(time(nullptr))); } async::result Instance::burst() { std::cout << "I'm done bursting too. Waiting for network informations..." << std::endl; co_await socket->send("VERSION\n"sv); co_await netInfo.wait_ready(); std::cout << "Ready." << std::endl; co_await send_ping(); client_bursting = false; } async::result Instance::process(const Command command) { std::clog << command.dump() << std::flush; if (command.name == "PASS") { // Check password auto given_password = strSplit(command.args, ' ', 1)[0]; if (given_password != config.auth.accept_password) { throw ConnectionError("Server supplied wrong password during authentication"); } authed = true; // Get server ID connected_server.uid = SUID(command.text); } else if (command.name == "ERROR") { throw ConnectionError(command.dump()); } else if (command.name == "SQUIT") { if (command.args == config.server.uid.str()) { throw ConnectionError(command.dump()); } } else if (!authed) { throw ConnectionError("Server tried to execute a command before authenticating"); } else if (command.name == "SERVER") { // Get name and description of connected server connected_server.name = strSplit(command.args, ' ', 1)[0]; connected_server.description = command.text; } else if (command.name == "PING") { // End of burst if (server_bursting) { server_bursting = false; std::cout << "Server burst is over. It's my turn." << std::endl; co_await burst(); } // Reply co_await socket->send(":{} PONG {} {} :{}\n"_format(config.server.uid.str(), config.server.name, command.args, command.text)); } } async::result Instance::process(Event event) { event.splitArgs(); std::clog << event.dump() << std::flush; if (!authed && event.name != "NOTICE") { throw ConnectionError("Server tried to send an event before authenticating"); } // Fetched info else if (event.name == "005") { // Check if that 005 was for me if (event.args[0] != config.server.uid.str()) { co_return; } // Split the list // Iterate and find the information we need for (const auto& field : event.args) { // Split into key and value auto split = strSplit(field, '=', 1); // Check size if (split.size() != 2) { continue; } // Check if we've got the right key if (split[0] == "NETWORK") { // Network name netInfo.name = std::move(split[1]); netInfo.fields_received++; } else if (split[0] == "CHANMODES") { // Channel modes auto modeLists = strSplit(split[1], ',', 3); netInfo.channelModes = { .listModes = std::string(modeLists[0]), .paramOnSetAndUnsetModes = std::string(modeLists[1]), .paramOnSetOnlyModes = std::string(modeLists[2]), .paramLessModes = std::string(modeLists[3]) }; netInfo.fields_received++; } else if (split[0] == "PREFIX") { auto val = split[1]; // User prefixes auto modesIt = val.find("(") + 1; auto prefixesIt = val.find(")") + 1; while (val[modesIt] != ')') { netInfo.channelModes.prefixMap[val[prefixesIt]] = val[modesIt]; netInfo.channelModes.listModes.push_back(val[modesIt]); modesIt++; prefixesIt++; } netInfo.fields_received++; } } // Check if everything needed has been fetched if (netInfo.fields_received == 3 && !netInfo.ready) { netInfo.mark_ready(); } } else { co_await netInfo.wait_ready(); // User updates if (event.name == "EUID") { // User connected to the network // Create user and parse event auto user = std::make_unique(); user->parse_euid(event, netInfo); // Append user to cache cache.users.push_back(std::move(user)); } else if (event.name == "QUIT") { // User disconnected from the network // Find user in cache auto res = cache.find_user_by_uid(std::get(event.sender.id)); if (res == cache.users.end()) { throw DesyncError(); } // Delete user from all channels for (auto& channel : res->get()->channels) { channel->removeMember(res->get()); } // Delete user from cache cache.users.erase(res); } else if (event.name == "NICK") { // User changed their nick // Find user in cache auto res = cache.find_user_by_uid(std::get(event.sender.id)); if (res == cache.users.end()) { throw DesyncError(); } // Set nick res->get()->nick = event.args[0]; } else if (event.name == "MODE") { // User changed their mode // Find user in cache auto res = cache.find_user_by_uid(std::get(event.sender.id)); if (res == cache.users.end()) { throw DesyncError(); } // Update mode res->get()->umode.parse(event.text, netInfo); } else if (event.name == "ENCAP") { // Get args argsSizeCheck("ENCAP", event.args, 3); if (event.args[1] == "SU") { // User logged in // Check args argsSizeCheck("ENCAP (SU)", event.args, 4); // Find user in cache auto res = cache.find_user_by_uid(event.args[2]); if (res == cache.users.end()) { throw DesyncError(); } // Update login name if (event.args.size() > 3) { auto loginName = event.args[3]; if (loginName.empty() || loginName == "*" || loginName == "0") { res->get()->loginName.reset(); } else { res->get()->loginName = event.args[3]; } } else { res->get()->loginName.reset(); } } } // Channel updates else if (event.name == "SJOIN") { // Channel was created // Create channel and parse event auto channel = std::make_unique(); channel->parse_sjoin(event, cache, netInfo); // Append channel to cache cache.channels.push_back(std::move(channel)); } else if (event.name == "TOPIC" || event.name == "TB") { // Channels topic changed // Find channel in cache auto res = cache.find_channel_by_name(event.args[0]); if (res == cache.channels.end()) { throw DesyncError(); } // Set topic res->get()->topic = event.text; } else if (event.name == "JOIN") { // User joined existing channel argsSizeCheck("JOIN", event.args, 3); // Get channel from cache auto c_res = cache.find_channel_by_name(event.args[1]); if (c_res == cache.channels.end()) { throw DesyncError(); } // Get user from cache auto u_res = cache.find_user_by_uid(std::get(event.sender.id)); if (u_res == cache.users.end()) { throw DesyncError(); } // Assign user to channel and vice versa c_res->get()->members.push_back(u_res->get()); u_res->get()->channels.push_back(c_res->get()); // Update channel modes c_res->get()->mode.parse(event.fromArgToEnd(event.args[2]), netInfo); } else if (event.name == "PART" || event.name == "KICK") { // User left channel // Get channel from cache auto c_res = cache.find_channel_by_name(event.args[0]); if (c_res == cache.channels.end()) { throw DesyncError(); } // Get user from cache auto u_res = cache.find_user_by_uid((event.name=="PART")?std::get(event.sender.id):event.args[1]); if (u_res == cache.users.end()) { throw DesyncError(); } // Remove channel from both user and channel c_res->get()->removeMember(u_res->get()); u_res->get()->removeChannel(c_res->get()); } else if (event.name == "TMODE" || event.name == "BMASK") { // Channel modes changed argsSizeCheck(event.name, event.args, 3); // Split args std::string_view modes, channelName; { channelName = event.args[1]; modes = event.fromArgToEnd(event.args[2]); } // Get channel from cache auto c_res = cache.find_channel_by_name(channelName); if (c_res == cache.channels.end()) { throw DesyncError(); } // Apply changes c_res->get()->mode.parse("+{}"_format(modes), netInfo); } // Messages else if (event.name == "PRIVMSG") { // On message // Check that message is not directed to channel if (isalnum(event.raw_args[0])) { // Get author from cache auto res = cache.find_user_by_uid(std::get(event.sender.id)); if (res == cache.users.end()) { throw DesyncError(); } // Get target UUID UUID target(event.args[0]); // Pass to services with ownership over target for (auto& service : services) { if (service->ready && service->uuid.str() == target.str()) { co_await service->on_direct_privmsg(event.text, res->get()); } } } } // Pass to services for (auto& service : services) { if (service->ready) { co_await service->on_event(event); } } } } async::result Instance::send_event(const Event& event) { co_await socket->send(event.dump()); co_await process(event); } async::result Instance::send_ping() { co_await socket->send("PING :{}\n"_format(connected_server.uid.str())); }