From 85d4852b0e1189fda926898bd52666e6b63fb6cf Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 23 Sep 2025 19:05:54 +0300 Subject: [PATCH 1/2] bug with user quiting and not using bot priviliages fixed --- bot/main.cpp | 16 ++++++++++++++++ src/Server.cpp | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bot/main.cpp b/bot/main.cpp index e346e61..d5d6601 100644 --- a/bot/main.cpp +++ b/bot/main.cpp @@ -469,6 +469,22 @@ int main(int argc, char **argv) { } } + if (msg->command == "QUIT") { + std::string quitter = extract_nick_from_prefix(msg->prefix); + for (auto &pair : channel_ops) { + pair.second.erase(quitter); + } + } + + if (msg->command == "PART" && msg->params.size() >= 1) { + std::string parter = extract_nick_from_prefix(msg->prefix); + const std::string &chan = msg->params[0]; + auto it = channel_ops.find(chan); + if (it != channel_ops.end()) { + it->second.erase(parter); + } + } + // INVITE handling: always attempt (re)JOIN if not already joined if (msg->command == "INVITE" && msg->params.size() >= 2) { const std::string &invitee = msg->params[0]; diff --git a/src/Server.cpp b/src/Server.cpp index 90e7cc7..7e39dc7 100644 --- a/src/Server.cpp +++ b/src/Server.cpp @@ -31,7 +31,7 @@ _password(passwd) throw std::runtime_error("Server::Server: ERROR - Failed listen on port " + port); } - _events.reserve(_max_events * sizeof(epoll_event)); + _events.resize(_max_events); _addOwnSocket(_sock); } From 5b1155ddac85d6e13d6588cf79b279df8f3d4567 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 23 Sep 2025 19:55:45 +0300 Subject: [PATCH 2/2] bot refactored to be a class, also some commands concerning filters added, now bot tracks filter in each channel separate, instead of global list --- Makefile | 2 +- bot/Bot.cpp | 347 ++++++++++++++++++++++++++++++++ bot/Bot.hpp | 62 ++++++ bot/main.cpp | 551 +++------------------------------------------------ 4 files changed, 443 insertions(+), 519 deletions(-) create mode 100644 bot/Bot.cpp create mode 100644 bot/Bot.hpp diff --git a/Makefile b/Makefile index 18d84e1..785581c 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ SRCS := $(addprefix src/, $(SRC)) OBJS := $(SRCS:src/%.cpp=.build/%.o) DEPS := $(OBJS:.o=.d) -BOT_SRC := bot/main.cpp +BOT_SRC := bot/main.cpp bot/Bot.cpp BOT_SRCS := $(BOT_SRC) src/RecvParser.cpp BOT_OBJS := $(BOT_SRC:bot/%.cpp=.build/bot_%.o) .build/RecvParser.o diff --git a/bot/Bot.cpp b/bot/Bot.cpp new file mode 100644 index 0000000..0c0a485 --- /dev/null +++ b/bot/Bot.cpp @@ -0,0 +1,347 @@ +#include "Bot.hpp" +#include "../inc/RecvParser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile std::sig_atomic_t g_stop = 0; +static volatile std::sig_atomic_t g_userInitiated = 0; +static void handle_stop(int) { g_stop = 1; g_userInitiated = 1; } + +Bot::Bot(const Config &c) +: sockfd(-1), cfg(c), currentNick(cfg.nick), baseNick(cfg.nick), autoJoined(false), + nick_attempt(0), backoff(2), backoff_max(30), retries(0) { + setup_builtin_commands(); +} + +Bot::~Bot() { + if (sockfd >= 0) ::close(sockfd); +} + +// ---------------- Public API ---------------- +void Bot::add_command(const std::string &key, std::function fn) { + commands[key] = fn; +} + + +int Bot::connect_socket() { + struct addrinfo hints; std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; + struct addrinfo *res = nullptr; + int rc = ::getaddrinfo(cfg.host.c_str(), cfg.port.c_str(), &hints, &res); + if (rc != 0) { std::cerr << "getaddrinfo: " << gai_strerror(rc) << "\n"; return -1; } + constexpr int CONNECT_TIMEOUT_MS = 5000; + int fd = -1; + for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { + int tmp = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (tmp < 0) continue; + int flags = ::fcntl(tmp, F_GETFL, 0); + if (flags >= 0) ::fcntl(tmp, F_SETFL, flags | O_NONBLOCK); + int r = ::connect(tmp, ai->ai_addr, ai->ai_addrlen); + if (r == 0) { fd = tmp; if (flags >= 0) ::fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); break; } + else if (r < 0 && errno == EINPROGRESS) { + struct pollfd pfd{tmp, POLLOUT, 0}; + int pr = ::poll(&pfd, 1, CONNECT_TIMEOUT_MS); + if (pr > 0 && (pfd.revents & POLLOUT)) { + int err = 0; socklen_t len = sizeof(err); + if (::getsockopt(tmp, SOL_SOCKET, SO_ERROR, &err, &len) == 0 && err == 0) { + fd = tmp; if (flags >= 0) ::fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); break; + } + } + } + ::close(tmp); + } + ::freeaddrinfo(res); + return fd; +} + +void Bot::login() { + if (!cfg.pass.empty()) send_line("PASS " + cfg.pass); + send_line("NICK " + currentNick); + send_line("USER " + currentNick + " 0 * :irc_hive bot"); +} + +void Bot::send_line(const std::string &s) { + std::cerr << ">> " << s << "\n"; + std::string wire = s + "\r\n"; + const char *p = wire.c_str(); size_t left = wire.size(); + while (left > 0) { + ssize_t n = ::send(sockfd, p, left, MSG_NOSIGNAL); + if (n < 0) { if (errno == EINTR) continue; std::cerr << "send failed: " << std::strerror(errno) << "\n"; return; } + left -= static_cast(n); p += n; + } +} + +void Bot::send_privmsg(const std::string &target, const std::string &text) { send_line("PRIVMSG " + target + " :" + text); } + +void Bot::join_channels() { for (const auto &ch : cfg.channels) if (!ch.empty()) send_line("JOIN " + ch); } + +std::string Bot::extract_nick_from_prefix(const std::optional &prefix) { + if (!prefix || prefix->empty()) { + return ""; + } + size_t excl = prefix->find('!'); + return (excl == std::string::npos) ? *prefix : prefix->substr(0, excl); +} + +void Bot::log_message(const Message &msg) { + std::cerr << "<< "; + if (msg.prefix) std::cerr << ":" << *msg.prefix << " "; + std::cerr << msg.command; + for (size_t i = 0; i < msg.params.size(); ++i) { + bool is_last = (i == msg.params.size() - 1); + if (is_last && msg.params[i].find(' ') != std::string::npos) std::cerr << " :" << msg.params[i]; + else std::cerr << " " << msg.params[i]; + } + std::cerr << "\n"; +} + +void Bot::setup_builtin_commands() { + auto reply = [&](const std::string &target, const std::string &text) { if (sockfd >= 0) send_privmsg(target, text); }; + + add_command("!ping", [&, reply](const std::string &target, const std::string &args){ (void)args; reply(target, "pong!"); }); + + add_command("!help", [&, reply](const std::string &target, const std::string &args){ (void)args; + reply(target, "/part {message} - leave the channel, with a message"); + reply(target, "/kick [nick] {message} - kick the user out of the channel, with a message"); + reply(target, "/quit {message} - disconnect from the server, with a message"); + reply(target, "/mode [l/o/t/k/i] - sets the channel modes, i-invite mode, o-operator, t-topic, l-limit, k-key"); + reply(target, "/msg [nick] - send message to user"); + reply(target, "/topic {name} - sets the topic of the channel to {name}"); + reply(target, "/invite [nick] {#channel_name} - invites user to the channel"); + reply(target, "/join {#channel_name} - joins the channel"); + reply(target, "/nick [nick] - tries to change the nick"); + reply(target, "!filter word ... - add words to channel filter"); + reply(target, "!unfilter word ... - remove words from channel filter"); + reply(target, "!listfilter - list channel filter words"); + }); + + add_command("!filter", [&, reply](const std::string &target, const std::string &args){ + if (target.empty() || target[0] != '#') { + std::string reply_target = (target == currentNick && !last_sender_nick.empty()) ? last_sender_nick : target; + reply(reply_target, "Filter can only be modified from a channel context"); + return; + } + auto chanIt = channel_ops.find(target); + if (chanIt == channel_ops.end() || chanIt->second.find(last_sender_nick) == chanIt->second.end()) { + reply(target, "You are not a channel operator - cannot modify filter"); + return; + } + if (args.empty()) { reply(target, "Usage: !filter word [word...]"); return; } + std::istringstream iss(args); std::string w; int added = 0; + auto &filter = channel_filters[target]; + while (iss >> w) { + std::string lw; lw.reserve(w.size()); + for (char c : w) lw.push_back(static_cast(std::tolower(static_cast(c)))); + if (!lw.empty()) if (filter.insert(lw).second) ++added; + } + reply(target, "Filter updated: " + std::to_string(filter.size()) + " words total (" + std::to_string(added) + " new)"); + }); + + add_command("!unfilter", [&, reply](const std::string &target, const std::string &args){ + if (target.empty() || target[0] != '#') { + std::string reply_target = (target == currentNick && !last_sender_nick.empty()) ? last_sender_nick : target; + reply(reply_target, "Filter can only be modified from a channel context"); + return; + } + auto chanIt = channel_ops.find(target); + if (chanIt == channel_ops.end() || chanIt->second.find(last_sender_nick) == chanIt->second.end()) { + reply(target, "You are not a channel operator - cannot modify filter"); + return; + } + if (args.empty()) { reply(target, "Usage: !unfilter word [word...]"); return; } + auto fit = channel_filters.find(target); + if (fit == channel_filters.end() || fit->second.empty()) { reply(target, "Filter empty"); return; } + std::istringstream iss(args); std::string w; int removed = 0; auto &filter = fit->second; + while (iss >> w) { + std::string lw; lw.reserve(w.size()); + for (char c : w) lw.push_back(static_cast(std::tolower(static_cast(c)))); + if (!lw.empty()) removed += filter.erase(lw); + } + reply(target, "Filter updated: " + std::to_string(filter.size()) + " words total (" + std::to_string(removed) + " removed)"); + }); + + add_command("!listfilter", [&, reply](const std::string &target, const std::string &args){ (void)args; + if (target.empty() || target[0] != '#') { + std::string reply_target = (target == currentNick && !last_sender_nick.empty()) ? last_sender_nick : target; + reply(reply_target, "Filter can only be listed from a channel context"); + return; + } + auto fit = channel_filters.find(target); + if (fit == channel_filters.end() || fit->second.empty()) { reply(target, "Filter empty"); return; } + std::string list; list.reserve(128); + for (std::unordered_set::const_iterator it = fit->second.begin(); it != fit->second.end(); ++it) { + if (!list.empty()) list.append(" "); + list.append(*it); + if (list.size() > 350) { list.append(" ..."); break; } + } + reply(target, "Filter(" + std::to_string(fit->second.size()) + "): " + list); + }); +} + +void Bot::handle_message(const Message &msg) { + log_message(msg); + + if (msg.command == "PING") { + std::string token = msg.params.empty() ? "" : msg.params[0]; + send_line("PONG :" + token); return; + } + + // Welcome / end of MOTD + if (msg.command == "001" || msg.command == "376" || msg.command == "422") { + if (!autoJoined && !cfg.channels.empty()) { join_channels(); autoJoined = true; } + return; + } + + if (msg.command == "433") { // nick in use + ++nick_attempt; currentNick = baseNick + "_" + std::to_string(nick_attempt); + std::cerr << "[bot] nick in use, trying " << currentNick << "\n"; + send_line("NICK " + currentNick); return; + } + + if (msg.command == "353" && msg.params.size() >= 4) { // RPL_NAMREPLY + const std::string &chan = msg.params[2]; + std::string names = msg.params[3]; if (!names.empty() && names[0] == ':') names.erase(0,1); + std::istringstream nss(names); std::string token; auto &ops = channel_ops[chan]; + while (nss >> token) if (!token.empty() && token[0] == '@') { std::string opNick = token.substr(1); if (!opNick.empty()) ops.insert(opNick); } + return; + } + + if (msg.command == "MODE" && msg.params.size() >= 2) { + const std::string &chan = msg.params[0]; if (!(chan.size() && chan[0] == '#')) return; + const std::string &modes = msg.params[1]; std::vector modeArgs; for (size_t i=2;i= 2) { + const std::string &target = msg.params[0]; const std::string &text = msg.params[1]; + std::string sender_nick = extract_nick_from_prefix(msg.prefix); + + if (!text.empty() && text[0] == '!') { // command + size_t spc = text.find(' '); + std::string key = (spc == std::string::npos) ? text : text.substr(0, spc); + std::string args = (spc == std::string::npos) ? "" : text.substr(spc + 1); + auto it = commands.find(key); + if (it != commands.end()) { + std::string reply_target = (target == currentNick && !sender_nick.empty()) ? sender_nick : target; + last_sender_nick = sender_nick; it->second(reply_target, args); + } + } + if (!sender_nick.empty() && sender_nick != currentNick && target.size() && target[0] == '#' && (text.empty() || text[0] != '!')) { + auto cfIt = channel_filters.find(target); + if (cfIt != channel_filters.end() && !cfIt->second.empty()) { + const auto &filter = cfIt->second; + std::string token; auto flush_token = [&]() { + if (token.empty()) return false; + if (filter.find(token) != filter.end()) { + send_line("KICK " + target + " " + sender_nick + " :filtered word"); + return true; + } + token.clear(); + return false; + }; + for (char ch : text) { + if (std::isalnum(static_cast(ch))) token.push_back(static_cast(std::tolower(static_cast(ch)))); + else if (flush_token()) break; + } + if (!token.empty()) flush_token(); + } + } + return; + } + + if (msg.command == "JOIN" && msg.params.size() >= 1) { + std::string selfNick = extract_nick_from_prefix(msg.prefix); + if (selfNick == currentNick) { const std::string &chan = msg.params[0]; if (!chan.empty()) joined_channels.insert(chan); } + return; + } + + if (msg.command == "QUIT") { + std::string quitter = extract_nick_from_prefix(msg.prefix); + for (auto &pair : channel_ops) pair.second.erase(quitter); + return; + } + + if (msg.command == "PART" && msg.params.size() >= 1) { + std::string parter = extract_nick_from_prefix(msg.prefix); + const std::string &chan = msg.params[0]; auto it = channel_ops.find(chan); if (it != channel_ops.end()) it->second.erase(parter); return; + } + + if (msg.command == "INVITE" && msg.params.size() >= 2) { + const std::string &invitee = msg.params[0]; const std::string &channel = msg.params[1]; + if (invitee == currentNick && channel.size()) { + if (std::find(cfg.channels.begin(), cfg.channels.end(), channel) == cfg.channels.end()) cfg.channels.push_back(channel); + if (joined_channels.find(channel) == joined_channels.end()) send_line("JOIN " + channel); + } + return; + } +} + +void Bot::send_quit(const std::string &reason) { + if (sockfd < 0) { + return; + } + send_line("QUIT :" + reason); + ::close(sockfd); + sockfd = -1; +} + +void Bot::run() { + std::signal(SIGINT, handle_stop); std::signal(SIGTERM, handle_stop); std::signal(SIGPIPE, SIG_IGN); + baseNick = cfg.nick; currentNick = cfg.nick; + + while (!g_stop) { + sockfd = connect_socket(); + if (sockfd < 0) { + ++retries; std::cerr << "[bot] connect failed (" << retries << "/" << MAX_RETRIES << "), retry in " << backoff << "s\n"; + if (retries >= MAX_RETRIES) { std::cerr << "[bot] maximum retries reached, exiting\n"; break; } + for (int s=0; s> msg_queue; RecvParser parser(msg_queue); + + while (!g_stop) { + struct pollfd pfd{sockfd, POLLIN, 0}; int pr = ::poll(&pfd, 1, 10000); + if (pr < 0) { if (errno == EINTR) continue; std::cerr << "poll error: " << std::strerror(errno) << "\n"; break; } + if (pr == 0) continue; + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { std::cerr << "[bot] socket closed (treating as server shutdown, exiting)\n"; g_stop = 1; break; } + if (pfd.revents & POLLIN) { + char buf[4096]; ssize_t n = ::recv(sockfd, buf, sizeof(buf), 0); + if (n <= 0) { std::cerr << "[bot] recv <= 0 (treating as server shutdown, exiting)\n"; ::close(sockfd); sockfd = -1; g_stop = 1; break; } + parser.feed(buf, static_cast(n)); + while (!msg_queue.empty()) { auto &ptr = msg_queue.front(); handle_message(*ptr); msg_queue.pop(); } + } + } + + if (g_stop && g_userInitiated) { send_quit("Client exiting"); struct pollfd tmp{sockfd, POLLIN, 0}; poll(&tmp, 1, 100); } + if (sockfd >= 0) { ::close(sockfd); sockfd = -1; } + if (g_stop) break; + + std::cerr << "[bot] reconnecting in " << backoff << "s\n"; for (int s=0; s +#include +#include +#include +#include +#include +#include +struct Message; + +class Bot { +public: + struct Config { + std::string host = "localhost"; + std::string port = "6667"; + std::string nick = "ircbot"; + std::string pass; + std::vector channels; + }; + + explicit Bot(const Config &cfg); + ~Bot(); + + void run(); + + void add_command(const std::string &key, + std::function fn); + +private: + int sockfd; + Config cfg; + std::string currentNick; + std::string baseNick; + + std::unordered_set joined_channels; + std::unordered_map> channel_filters; // channel -> banned words + std::unordered_map> channel_ops; // channel -> ops + std::string last_sender_nick; + bool autoJoined; // joined channels after welcome numeric + + int nick_attempt; + int backoff; + int backoff_max; + int retries; + const int MAX_RETRIES = 5; + + // Commands + std::unordered_map> commands; + + int connect_socket(); + void login(); + void send_line(const std::string &s); + void send_privmsg(const std::string &target, const std::string &text); + void join_channels(); + + std::string extract_nick_from_prefix(const std::optional &prefix); + void log_message(const Message &msg); + void handle_message(const Message &msg); + void send_quit(const std::string &reason); + + void setup_builtin_commands(); +}; diff --git a/bot/main.cpp b/bot/main.cpp index d5d6601..bd8fb7e 100644 --- a/bot/main.cpp +++ b/bot/main.cpp @@ -1,527 +1,42 @@ -#include -#include -#include -#include -#include -#include -#include +#include "Bot.hpp" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../inc/RecvParser.hpp" - -inline constexpr std::array HELP_LINES = { - "/part {message} - leave the channel, with a message", - "/kick [nick] {message} - kick the user out of the channel, with a message", - "/quit {message} - disconnect from the server, with a message", - "/mode [l/o/t/k/i] - sets the channel modes, i-invite mode, o-operator, t-topic, l-limit, k-key", - "/msg [nick] - send message to user", - "/topic {name} - sets the topic of the channel to {name}", - "/invite [nick] {#channel_name} - invites user to the channel", - "/join {#channel_name} - joins the channel", - "/nick [nick] - tries to change the nick" -}; - - - -static volatile std::sig_atomic_t g_stop = 0; -static volatile std::sig_atomic_t g_userInitiated = 0; -static void handle_stop(int) { g_stop = 1; g_userInitiated = 1; } - -static void fatal_usage() { - std::cerr << "Usage: ./ircbot [options]\n" - << " -s HOST IRC server (default: localhost)\n" - << " -p PORT IRC port (default: 6667)\n" - << " -n NICK Nickname (default: ircbot)\n" - << " -c CHS Comma-separated channels, e.g. \"#test,#bots\"\n" - << " --pass PASS Server password\n"; - std::exit(2); -} - -static void parse_args(int argc, char **argv, std::string &host, - std::string &port, std::string &nick, - std::vector &channels, std::string &pass) { - host = "localhost"; - port = "6667"; - nick = "ircbot"; - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - auto need = [&](std::string &out) { - if (i + 1 >= argc) { - fatal_usage(); - } - out = argv[++i]; - }; - if (a == "-s") - need(host); - else if (a == "-p") - need(port); - else if (a == "-n") - need(nick); - else if (a == "-c") { - std::string chs; - need(chs); - size_t start = 0; - while (true) { - size_t pos = chs.find(',', start); - std::string token = (pos == std::string::npos) - ? chs.substr(start) - : chs.substr(start, pos - start); - if (!token.empty()) - channels.push_back(token); - if (pos == std::string::npos) - break; - start = pos + 1; - } - } else if (a == "--pass") - need(pass); - else { - std::cerr << "Unknown arg: " << a << "\n"; - fatal_usage(); - } - } -} - -static int connect_to(const std::string &host, const std::string &port) { - struct addrinfo hints; - std::memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - struct addrinfo *res = nullptr; - int rc = ::getaddrinfo(host.c_str(), port.c_str(), &hints, &res); - if (rc != 0) { - std::cerr << "getaddrinfo: " << gai_strerror(rc) << "\n"; - return -1; - } - constexpr int CONNECT_TIMEOUT_MS = 5000; - int fd = -1; - for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { - int tmp = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); - if (tmp < 0) - continue; - int flags = ::fcntl(tmp, F_GETFL, 0); - if (flags >= 0) - ::fcntl(tmp, F_SETFL, flags | O_NONBLOCK); - int r = ::connect(tmp, ai->ai_addr, ai->ai_addrlen); - if (r == 0) { - fd = tmp; // Connected immediately - if (flags >= 0) ::fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); - break; - } else if (r < 0 && errno == EINPROGRESS) { - struct pollfd pfd{tmp, POLLOUT, 0}; - int pr = ::poll(&pfd, 1, CONNECT_TIMEOUT_MS); - if (pr > 0 && (pfd.revents & POLLOUT)) { - int err = 0; - socklen_t len = sizeof(err); - if (::getsockopt(tmp, SOL_SOCKET, SO_ERROR, &err, &len) == 0 && err == 0) { - fd = tmp; - if (flags >= 0) ::fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); - break; - } - } - } - ::close(tmp); - } - ::freeaddrinfo(res); - return fd; -} - -static bool send_line(int fd, const std::string &s) { - std::cerr << ">> " << s << "\n"; - std::string wire = s; - wire += "\r\n"; - const char *p = wire.c_str(); - size_t left = wire.size(); - while (left > 0) { - ssize_t n = ::send(fd, p, left, MSG_NOSIGNAL); - if (n < 0) { - if (errno == EINTR) - continue; - std::cerr << "send failed: " << std::strerror(errno) << "\n"; - return false; - } - left -= static_cast(n); - p += n; - } - return true; -} - -static bool send_privmsg(int fd, const std::string &target, - const std::string &text) { - return send_line(fd, "PRIVMSG " + target + " :" + text); -} - -static void join_channels(int fd, const std::vector &channels) { - for (const auto &ch : channels) - if (!ch.empty()) - send_line(fd, "JOIN " + ch); -} - -static std::string extract_nick_from_prefix(const std::optional &prefix) { - if (!prefix || prefix->empty()) - return ""; - size_t excl = prefix->find('!'); - return (excl == std::string::npos) ? *prefix : prefix->substr(0, excl); -} - -static void log_message(const Message &msg) { - std::cerr << "<< "; - if (msg.prefix) - std::cerr << ":" << *msg.prefix << " "; - std::cerr << msg.command; - for (size_t i = 0; i < msg.params.size(); ++i) { - bool is_last = (i == msg.params.size() - 1); - if (is_last && msg.params[i].find(' ') != std::string::npos) - std::cerr << " :" << msg.params[i]; - else - std::cerr << " " << msg.params[i]; - } - std::cerr << "\n"; -} +#include -static void send_quit(int fd, const std::string &reason) { - if (fd < 0) return; - send_line(fd, "QUIT :" + reason); - ::close(fd); - /* ::shutdown(fd, SHUT_WR); */ +static void usage() { + std::cerr << "Usage: ./ircbot [options]\n" + << " -s HOST IRC server (default: localhost)\n" + << " -p PORT IRC port (default: 6667)\n" + << " -n NICK Nickname (default: ircbot)\n" + << " -c CHS Comma-separated channels, e.g. \"#test,#bots\"\n" + << " --pass PASS Server password\n"; } int main(int argc, char **argv) { - std::signal(SIGINT, handle_stop); - std::signal(SIGTERM, handle_stop); - std::signal(SIGPIPE, SIG_IGN); - - std::string host, port, nick, pass; - std::vector channels; - parse_args(argc, argv, host, port, nick, channels, pass); - - const std::string base_nick = nick; - int nick_attempt = 0; - int backoff = 2, backoff_max = 30; - int retries = 0; - const int MAX_RETRIES = 5; - std::unordered_set joined_channels; - std::unordered_set filter_words; - // Track channel operators (updated from 353 and MODE +o/-o) - std::unordered_map> channel_ops; - // Last command sender (set when a bot command is processed) - std::string last_sender_nick; - - using Cmd = std::function; - std::unordered_map commands; - - int sockfd = -1; - auto reply = [&](const std::string &target, const std::string &text) { - if (sockfd >= 0) - send_privmsg(sockfd, target, text); - }; - commands["!ping"] = [&](const std::string &target, const std::string &args) { - (void)args; - reply(target, "pong!"); - }; - - commands["!help"] = [&](const std::string &target, const std::string &args) { - (void)args; - for (std::string_view line : HELP_LINES) { - reply(target, std::string(line)); - } - }; - - commands["!filter"] = [&](const std::string &target, const std::string &args) { - // Determine the channel context (only allow in a channel) - if (target.empty() || target[0] != '#') { - reply((target == nick ? last_sender_nick : target), "Filter can only be modified from a channel context"); - return; - } - // Require that the sender is a known channel operator - auto chanIt = channel_ops.find(target); - if (chanIt == channel_ops.end() || chanIt->second.find(last_sender_nick) == chanIt->second.end()) { - reply(target, "You are not a channel operator - cannot modify filter"); - return; - } - std::istringstream iss(args); - std::string w; - int added = 0; - while (iss >> w) { - std::string lw; - lw.reserve(w.size()); - for (char c : w) - lw.push_back(static_cast(std::tolower(static_cast(c)))); - if (!lw.empty()) { - if (filter_words.insert(lw).second) - ++added; - } - } - reply(target, "Filter updated: " + std::to_string(filter_words.size()) + - " words total (" + std::to_string(added) + " new)"); - }; - - while (!g_stop) { - sockfd = connect_to(host, port); - if (sockfd < 0) { - ++retries; - std::cerr << "[bot] connect failed (" << retries << "/" << MAX_RETRIES << "), retry in " << backoff << "s\n"; - if (retries >= MAX_RETRIES) { - std::cerr << "[bot] maximum retries reached, exiting\n"; - break; - } - for (int s = 0; s < backoff && !g_stop; ++s) - sleep(1); - backoff = std::min(backoff_max, backoff * 2); - continue; - } - std::cerr << "[bot] connected\n"; - retries = 0; - backoff = 2; - - if (!pass.empty()) - send_line(sockfd, "PASS " + pass); - send_line(sockfd, "NICK " + nick); - send_line(sockfd, "USER " + nick + " 0 * :irc_hive bot"); - - bool joined = false; - - std::queue> msg_queue; - RecvParser parser(msg_queue); - - while (!g_stop) { - struct pollfd pfd{sockfd, POLLIN, 0}; - int pr = ::poll(&pfd, 1, 10000); - if (pr < 0) { - if (errno == EINTR) - continue; - std::cerr << "poll error: " << std::strerror(errno) << "\n"; - break; - } - if (pr == 0) - continue; - if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { - std::cerr << "[bot] socket closed (treating as server shutdown, exiting)\n"; - g_stop = 1; - break; - } - if (pfd.revents & POLLIN) { - char buf[4096]; - ssize_t n = ::recv(sockfd, buf, sizeof(buf), 0); - if (n <= 0) { - std::cerr << "[bot] recv <= 0 (treating as server shutdown, exiting)\n"; - ::close(sockfd); - g_stop = 1; - break; - } - parser.feed(buf, static_cast(n)); - - while (!msg_queue.empty()) { - std::unique_ptr &msg = msg_queue.front(); - - log_message(*msg); - - // PING - if (msg->command == "PING") { - std::string token = msg->params.empty() ? "" : msg->params[0]; - send_line(sockfd, "PONG :" + token); - msg_queue.pop(); - continue; - } - - // Welcome / end of MOTD / no MOTD - if (msg->command == "001" || msg->command == "376" || msg->command == "422") { - if (!joined && !channels.empty()) { - join_channels(sockfd, channels); - joined = true; + Bot::Config cfg; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + auto need = [&](std::string &out) { + if (i + 1 >= argc) { usage(); std::exit(2); } + out = argv[++i]; + }; + if (a == "-s") need(cfg.host); + else if (a == "-p") need(cfg.port); + else if (a == "-n") need(cfg.nick); + else if (a == "-c") { + std::string chs; need(chs); + size_t start = 0; + while (true) { + size_t pos = chs.find(',', start); + std::string token = (pos == std::string::npos) ? chs.substr(start) : chs.substr(start, pos - start); + if (!token.empty()) cfg.channels.push_back(token); + if (pos == std::string::npos) break; + start = pos + 1; } - msg_queue.pop(); - continue; - } - - // Nick in use - if (msg->command == "433") { - ++nick_attempt; - nick = base_nick + "_" + std::to_string(nick_attempt); - std::cerr << "[bot] nick in use, trying " << nick << "\n"; - send_line(sockfd, "NICK " + nick); - msg_queue.pop(); - continue; - } - - // Update channel operators from RPL_NAMREPLY (353) - // Typical: 353 = #channel :@Op1 +VoicedUser User2 - if (msg->command == "353" && msg->params.size() >= 4) { - const std::string &chan = msg->params[2]; - const std::string &namesField = msg->params[3]; - // namesField may already be without leading ':' depending on parser - std::string names = namesField; - if (!names.empty() && names[0] == ':') - names.erase(0, 1); - std::istringstream nss(names); - std::string token; - auto &ops = channel_ops[chan]; // creates if not present - while (nss >> token) { - if (!token.empty() && (token[0] == '@')) { - std::string opNick = token.substr(1); - if (!opNick.empty()) - ops.insert(opNick); - } - } - } - - // Track MODE +o / -o changes: :setter MODE #channel +o nick OR grouped like +oo nick1 nick2 - if (msg->command == "MODE" && msg->params.size() >= 2) { - const std::string &chan = msg->params[0]; - if (!chan.empty() && chan[0] == '#') { - const std::string &modes = msg->params[1]; - // Remaining params are the nick targets consumed by 'o' (and others we ignore) - std::vector modeArgs; - for (size_t i = 2; i < msg->params.size(); ++i) - modeArgs.push_back(msg->params[i]); - - bool adding = true; - size_t argIndex = 0; - auto &ops = channel_ops[chan]; // creates if missing - for (char c : modes) { - if (c == '+') { adding = true; continue; } - if (c == '-') { adding = false; continue; } - if (c == 'o') { - if (argIndex < modeArgs.size()) { - const std::string &targetNick = modeArgs[argIndex++]; - if (!targetNick.empty()) { - if (adding) - ops.insert(targetNick); - else - ops.erase(targetNick); - } - } - } else { - // For other mode letters that take arguments, still consume if needed - // but since we only care about 'o', we just attempt to advance if letter uses an argument. - // Simple heuristic: ignore; most other user modes won't break ordering here for small scale. - } - } - } - } - - // PRIVMSG handling (commands + filter enforcement) - if (msg->command == "PRIVMSG" && msg->params.size() >= 2) { - const std::string &target = msg->params[0]; - const std::string &text = msg->params[1]; - std::string sender_nick = extract_nick_from_prefix(msg->prefix); - - // Bot commands (prefix '!') - if (!text.empty() && text[0] == '!') { - size_t spc = text.find(' '); - std::string key = (spc == std::string::npos) ? text : text.substr(0, spc); - std::string args = (spc == std::string::npos) ? "" : text.substr(spc + 1); - auto it = commands.find(key); - if (it != commands.end()) { - std::string reply_target = - (target == nick && !sender_nick.empty()) ? sender_nick : target; - // Record sender for privilege checks (e.g. !filter) - last_sender_nick = sender_nick; - it->second(reply_target, args); - } - } - - // Content filter (only for channel messages, not from the bot itself) - if (!filter_words.empty() && !sender_nick.empty() && sender_nick != nick && - !target.empty() && target[0] == '#' && - (text.empty() || text[0] != '!')) { - // Tokenize text into words (alnum sequences) - std::string token; - auto flush_token = [&](void) { - if (token.empty()) return false; - if (filter_words.find(token) != filter_words.end()) { - send_line(sockfd, "KICK " + target + " " + sender_nick + " :filtered word"); - return true; - } - token.clear(); - return false; - }; - for (char ch : text) { - if (std::isalnum(static_cast(ch))) { - token.push_back(static_cast(std::tolower(static_cast(ch)))); - } else { - if (flush_token()) - break; - } - } - if (!token.empty()) - flush_token(); - } - } - - // Track successful self JOIN to mark channel as joined - if (msg->command == "JOIN" && msg->params.size() >= 1) { - std::string selfNick = extract_nick_from_prefix(msg->prefix); - if (selfNick == nick) { - const std::string &chan = msg->params[0]; - if (!chan.empty()) - joined_channels.insert(chan); - } - } - - if (msg->command == "QUIT") { - std::string quitter = extract_nick_from_prefix(msg->prefix); - for (auto &pair : channel_ops) { - pair.second.erase(quitter); - } - } - - if (msg->command == "PART" && msg->params.size() >= 1) { - std::string parter = extract_nick_from_prefix(msg->prefix); - const std::string &chan = msg->params[0]; - auto it = channel_ops.find(chan); - if (it != channel_ops.end()) { - it->second.erase(parter); - } - } - - // INVITE handling: always attempt (re)JOIN if not already joined - if (msg->command == "INVITE" && msg->params.size() >= 2) { - const std::string &invitee = msg->params[0]; - const std::string &channel = msg->params[1]; - if (invitee == nick && !channel.empty()) { - // Track desired channel list (only add once) - if (std::find(channels.begin(), channels.end(), channel) == channels.end()) { - channels.push_back(channel); - } - // Re-attempt join if we are not yet in joined set - if (joined_channels.find(channel) == joined_channels.end()) { - send_line(sockfd, "JOIN " + channel); - } - } - } - - msg_queue.pop(); - } - } - } - - if (g_stop && g_userInitiated) { - send_quit(sockfd, "Client exiting"); - struct pollfd tmp{sockfd, POLLIN, 0}; - poll(&tmp, 1, 100); + } else if (a == "--pass") need(cfg.pass); + else { std::cerr << "Unknown arg: " << a << "\n"; usage(); std::exit(2); } } - ::close(sockfd); - sockfd = -1; - if (g_stop) - break; - std::cerr << "[bot] reconnecting in " << backoff << "s\n"; - for (int s = 0; s < backoff && !g_stop; ++s) - sleep(1); - backoff = std::min(backoff_max, backoff * 2); - nick = base_nick; - nick_attempt = 0; - } - return 0; + Bot bot(cfg); + bot.run(); + return 0; }