mirror of
https://gitlab.com/niansa/asbots.git
synced 2025-03-06 20:48:25 +01:00
541 lines
19 KiB
C++
541 lines
19 KiB
C++
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "instance.hpp"
|
|
#include "config.hpp"
|
|
#include "uid.hpp"
|
|
#include "utility.hpp"
|
|
|
|
#include <fmt/format.h>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <string_view>
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <ctime>
|
|
|
|
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<bool channelModes>
|
|
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<SUID>(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<false>(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 u_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<SUID>(event.sender.id);
|
|
// Check size
|
|
argsSizeCheck("SJOIN", event.args, 3);
|
|
// Move values
|
|
name = std::move(event.args[1]);
|
|
mode.parse<true>(event.args[2], netInfo);
|
|
// Get members
|
|
for (auto& raw_uuid : strSplit(event.text, ' ')) {
|
|
// Erase prefix
|
|
if (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();
|
|
}
|
|
u_User& member = *res;
|
|
// Append member to list
|
|
members.push_back(&member);
|
|
}
|
|
}
|
|
|
|
void Channel::removeMember(const u_User& member) {
|
|
std::remove_if(members.begin(), members.end(), [&] (auto obj) {return *obj == member;});
|
|
}
|
|
|
|
|
|
std::vector<u_User>::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<u_User>::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<u_Channel>::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<void> 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<std::string_view::size_type>(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<void> 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<void> 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<void> 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<void> 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>();
|
|
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<UUID>(event.sender.id));
|
|
if (res == cache.users.end()) {
|
|
throw DesyncError();
|
|
}
|
|
// Delete user from all channels
|
|
for (auto& channel : res->get()->channels) {
|
|
channel->get()->removeMember({*res});
|
|
}
|
|
// 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<UUID>(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<UUID>(event.sender.id));
|
|
if (res == cache.users.end()) {
|
|
throw DesyncError();
|
|
}
|
|
// Update mode
|
|
res->get()->umode.parse<false>(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>();
|
|
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<UUID>(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);
|
|
u_res->get()->channels.push_back(&*c_res);
|
|
// Update channel modes
|
|
c_res->get()->mode.parse<true>(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<UUID>(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);
|
|
u_res->get()->removeChannel(*c_res);
|
|
} 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.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<true>("+{} {}"_format(modes, event.text), 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<UUID>(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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Pass to services
|
|
for (auto& service : services) {
|
|
if (service->ready) {
|
|
co_await service->on_event(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async::result<void> Instance::send_event(const Event& event) {
|
|
co_await socket->send(event.dump());
|
|
co_await process(event);
|
|
}
|
|
|
|
async::result<void> Instance::send_ping() {
|
|
co_await socket->send("PING :{}\n"_format(connected_server.uid.str()));
|
|
}
|