// Bot.C -*- C++ -*- // Copyright (c) 1997, 1998 Etienne BERNARD // Copyright (C) 2002,2003,2005 Clinton Ebadi // 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 2 of the License, or // 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, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA // 02110-1301, USA. #include #include #include #include #include #include #include #include #include #include #include #ifdef USESCRIPTS #include #endif #include "Bot.H" #include "DCCConnection.H" #include "DCCChatConnection.H" #include "ChannelList.H" #include "DCCManager.H" #include "DCCPerson.H" #include "Parser.H" #include "Person.H" #include "Server.H" #include "ServerConnection.H" #include "ServerList.H" #include "ShitList.H" #include "StringTokenizer.H" #include "User.H" #include "UserCommands.H" #include "UserList.H" #include "Utils.H" #ifdef USESCRIPTS #include "BotInterp.H" #include "Interp.H" #endif unsigned int Bot::MAX_MESSAGES = 2; unsigned int Bot::MAX_NICKLENGTH = 9; #define DEFAULT_NICKNAME "Bobot" #define DEFAULT_USERNAME "bobot" #define DEFAULT_IRCNAME "I'm a bobot++!" #define DEFAULT_COMMANDCHAR '!' #define DEFAULT_USERLISTFILENAME "bot.users" #define DEFAULT_SHITLISTFILENAME "bot.shit" #define DEFAULT_HELPFILENAME "bot.help" #define DEFAULT_SCRIPTLOGFILENAME "script.log" #define DEFAULT_LOGFILENAME "bot.log" #define DEFAULT_LOGDIR getenv ("HOME") + String("/.bobotpp/logs/") #define DEFAULT_INITFILENAME "bot.init" #ifdef USESCRIPTS #define DEFAULT_AUTOEXECFILENAME "bot.autoexec" #endif Bot::Bot(String filename, bool debug_on) : nickName(DEFAULT_NICKNAME), wantedNickName(DEFAULT_NICKNAME), userName(DEFAULT_USERNAME), ircName(DEFAULT_IRCNAME), versionString(VERSION_STRING), userHost(""), localIP(""), commandChar(DEFAULT_COMMANDCHAR), userListFileName(DEFAULT_USERLISTFILENAME), shitListFileName(DEFAULT_SHITLISTFILENAME), helpFileName(DEFAULT_HELPFILENAME), initFileName(DEFAULT_INITFILENAME), connected(false), debug(debug_on), stop(false), sentPing(false), startTime(time(NULL)), currentTime(startTime), lastNickNameChange(startTime), lastChannelJoin(startTime), serverConnection(0), sentUserhostID(0), receivedUserhostID(0), logFileName(DEFAULT_LOGFILENAME), logs_dir (DEFAULT_LOGDIR), configFileName (filename) #ifdef USESCRIPTS ,scriptLogFileName(DEFAULT_SCRIPTLOGFILENAME), autoexecFileName(DEFAULT_AUTOEXECFILENAME) #endif { #ifdef HAVE_STL_CLEAR wantedChannels.clear(); ignoredUserhosts.clear(); spyList.clear(); userhostMap.clear(); #endif init_user_functions (); set_log_dir (logs_dir); set_log_file (logFileName); channelList = new ChannelList(); serverList = new ServerList(); readConfig(); userList = new UserList(userListFileName); shitList = new ShitList(shitListFileName); dccConnections = new DCCManager (); // Let's read the alias file std::ifstream initFile(initFileName.c_str ()); if (initFile) { // FIXME: these variables are current String instead of // std::string because String>> reads an entire line. This code // needs to be rewritten to use std::string and std::getline (or // better yet, be removed entirely once BotConfig is in place) String temp, alias, command; int line = 0; while (initFile >> temp, temp.length() != 0) { StringTokenizer st(temp); line++; temp = Utils::trim_str (temp); if (temp[0]=='#') { continue; } if (st.count_tokens (' ') != 2) { std::cerr << "Error when reading alias file (" << initFileName << ") line " << line << "...\n"; continue; } alias = Utils::to_upper (st.next_token()); command = Utils::to_upper (st.next_token()); // Does the function already exist ? if (!userFunctions[alias]) { if (userFunction *u = userFunctions[command]) userFunctions[alias] = new userFunction(u->function, u->minLevel, u->needsChannelName); } } } std::srand (std::time (0)); // srand for bot-random #ifdef USESCRIPTS botInterp = new BotInterp(this, logs_dir + scriptLogFileName); Interp::Startup2 (this); botInterp->LoadScript(autoexecFileName); #endif } Bot::~Bot() { Person *p; while (spyList.size() != 0) { p = (*spyList.begin()).second; spyList.erase(spyList.begin()); delete p; } delete dccConnections; destroy_user_functions (); wantedChannel *w; while (wantedChannels.size() != 0) { w = (*wantedChannels.begin()).second; wantedChannels.erase(wantedChannels.begin()); delete w; } userList->save(); shitList->save(); delete channelList; delete userList; delete serverList; delete shitList; delete serverConnection; logLine("Stopping log."); logFile.close(); } void Bot::logLine(String line) { tm *d; std::time_t current_time = time(0); d = localtime(¤t_time); logFile << "[" << std::setfill('0') << std::setw(2) << d->tm_mday << "/" << std::setfill('0') << std::setw(2) << d->tm_mon + 1 << "/" << d->tm_year + 1900 << " - " << std::setfill('0') << std::setw(2) << d->tm_hour << ":" << std::setfill('0') << std::setw(2) << d->tm_min << ":" << std::setfill('0') << std::setw(2) << d->tm_sec << "] " << line << std::endl; } void Bot::readConfig() { std::ifstream file(configFileName.c_str ()); String temp; int line = 1; if (!file) { logLine(String("I cannot find the file ") + configFileName); return; } while (!file.eof()) { file >> temp; if (temp.length() == 0 || temp[(unsigned int)0] == '#') { line++; continue; } StringTokenizer st(temp); String command = Utils::to_upper (Utils::trim_str (st.next_token('='))); String parameters = Utils::trim_str (st.next_token('=')); if (command == "NICK" || command == "NICKNAME") nickName = wantedNickName = parameters; else if (command == "USERNAME") userName = parameters; else if (command == "IRCNAME" || command == "REALNAME") ircName = parameters; else if (command == "CMDCHAR" || command == "COMMAND") commandChar = parameters[(unsigned int)0]; else if (command == "USERLIST") userListFileName = parameters; else if (command == "SHITLIST") shitListFileName = parameters; else if (command == "CHANNEL") { if (parameters.indexOf(':') == -1) { std::cout << "Warning. The 'channel' syntax has changed." << " Please see the README file for more information." << " I will use compatibility mode, but you're really" << " missing something.\n"; StringTokenizer st2(parameters); String name = Utils::to_lower (st2.next_token()); String key = st2.next_token(); wantedChannels[name] = new wantedChannel("", "", key); } else { StringTokenizer st2(parameters); String name = Utils::to_lower (st2.next_token(':')); String mode = st2.next_token(':'); String keep = st2.next_token(':'); String key = st2.next_token(':'); wantedChannels[name] = new wantedChannel(mode, keep, key); } } else if (command == "LOGFILE") { if (parameters != logFileName) { if (parameters[(unsigned int)0] == '/') { StringTokenizer log_st (parameters); std::string log_dir = "/"; for (unsigned int m = log_st.count_tokens ('/'); --m; m > 0) { log_dir += log_st.next_token ('/') + "/"; } std::cerr << "==" << log_dir << std::endl; set_log_dir (log_dir); set_log_file (log_st.rest ()); } else set_log_file (parameters); } } #ifdef USESCRIPTS else if (command == "SCRIPTLOGFILE") scriptLogFileName = parameters; else if (command == "AUTOEXECFILE") autoexecFileName = parameters; #endif else if (command == "INITFILE") initFileName = parameters; else if (command == "LOCALIP") localIP = parameters; else if (command == "MAXNICKLENGTH") MAX_NICKLENGTH = std::atoi (parameters.c_str ()); else if (command == "SERVER") { if (parameters.indexOf(' ') == -1) serverList->addServer(new Server(parameters)); else { StringTokenizer st2(parameters); String name = st2.next_token(); int port = std::atoi(st2.next_token().c_str()); serverList->addServer(new Server(name, port, st2.next_token())); } } else if (command == "") { // do nothing } else { logLine(String("Syntax error in file ") + configFileName + ", line " + String((long)line)); file.close(); std::exit(1); } line++; } file.close(); } void Bot::run() { nextServer(); while (!stop) { waitForInput(); // This is the main event loop dccConnections->checkStale (); if (!serverConnection->queue->flush()) { // Disconnected #ifdef USESCRIPTS // Run hooks/disconnect this->botInterp->RunHooks (Hook::DISCONNECT, serverConnection->server->getHostName (), scm_list_n (Utils::str2scm (serverConnection->server->getHostName ()), SCM_BOOL_F)); #endif nextServer(); } } } void Bot::waitForInput() { #ifdef _HPUX_SOURCE int rd; #else fd_set rd; #endif struct timeval timer; int sock = serverConnection->getFileDescriptor(); int maxSocketNumber = sock; #ifdef _HPUX_SOURCE rd = sock; #else FD_ZERO(&rd); FD_SET(sock, &rd); #endif DCC_MAP* dccmap = &dccConnections->dcc_map; for (DCC_MAP::iterator it = dccmap->begin (); it != dccmap->end(); ++it) { int s = it->second->dcc->getFileDescriptor(); #ifdef _HPUX_SOURCE rd |= s; #else FD_SET(s, &rd); #endif if (s > maxSocketNumber) maxSocketNumber = s; } timer.tv_sec = 1; timer.tv_usec = 0; switch (select(maxSocketNumber + 1, &rd, NULL, NULL, &timer)) { case 0: /* timeout */ break; case -1: /* error */ break; default: /* normal */ #ifdef _HPUX_SOURCE if (rd & sock) #else if (FD_ISSET(sock, &rd)) #endif if (serverConnection->handleInput()) nextServer(); dccConnections->checkInput (rd); } if (currentTime < std::time(0)) { // Actions that we do each second currentTime = std::time(0); for (std::map >::iterator it = ignoredUserhosts.begin(); it != ignoredUserhosts.end(); ++it) if ((*it).second > 0) (*it).second--; for (std::map >::iterator it = channelList->begin (); it != channelList->end (); ++it) { it->second->purge_expired_bans (); } #ifdef USESCRIPTS botInterp->RunTimers(currentTime); tm *thisTime = localtime(¤tTime); if (thisTime->tm_sec == 0) { char s[6]; std::snprintf(s, 6, "%02d:%02d", thisTime->tm_hour, thisTime->tm_min); botInterp->RunHooks(Hook::TIMER, String(s), scm_list_n (Utils::str2scm (std::string (s)), SCM_UNDEFINED)); } #endif } if (currentTime >= (time_t)(lastNickNameChange + Bot::NICK_CHANGE) && nickName != wantedNickName) { lastNickNameChange = currentTime; serverConnection->queue->sendNick(wantedNickName); } if (currentTime >= (std::time_t)(lastChannelJoin + Bot::CHANNEL_JOIN)) { lastChannelJoin = currentTime; for (std::map >::iterator it = wantedChannels.begin(); it != wantedChannels.end(); ++it) if (channelList->getChannel((*it).first) == 0) serverConnection->queue->sendJoin((*it).first, (*it).second->key); } if (currentTime >= (std::time_t)(serverConnection->serverLastSpoken + Bot::PING_TIME) && !sentPing) { serverConnection->queue->sendPing("Testing connection"); sentPing = true; } if (currentTime >= (std::time_t)(serverConnection->serverLastSpoken + Bot::TIMEOUT)) { sentPing = false; nextServer(); } } // We can change server if we will not lose op on a channel bool Bot::canChangeServer() { String channel; Channel *c; for (std::map >::iterator it = channelList->begin(); it != channelList->end(); ++it) { channel = (*it).first; c = channelList->getChannel(channel); if (c->countOp == 1 && c->count > 1 && this->iAmOp(channel)) return false; } return true; } void Bot::nextServer() { bool cont = false; if (channelList) channelList->clear(); if (serverConnection) userList->removeFirst(); delete serverConnection; do { Server * s = serverList->nextServer(); if (!s) { std::cout << "No server found. Exiting..." << std::endl; std::exit(1); } serverConnection = new ServerConnection(this, s, localIP); if (!serverConnection->connect()) { cont = true; // We sleep 10 seconds, to avoid connection flood sleep(10); delete serverConnection; } else { cont = false; } } while (cont); } void Bot::reconnect() { if (channelList) channelList->clear(); userList->removeFirst(); delete serverConnection; serverConnection = new ServerConnection(this, serverList->currentServer(), localIP); serverConnection->connect(); } void Bot::connect(int serverNumber) { if (channelList) channelList->clear(); userList->removeFirst(); delete serverConnection; serverConnection = new ServerConnection(this, serverList->get(serverNumber), localIP); serverConnection->connect(); } void Bot::addDCC(Person * from, unsigned long address, int port, int type) { DCCConnection *d = 0; if (type == CHAT) { d = new DCCChatConnection(this, from->getAddress (), address, port); } else { return; } if (!d->connect()) { logLine ("DCC Connection failed from " + from->getAddress ()); return; } logLine ("DCC CHAT accepted from" + from->getAddress ()); dccConnections->addConnection (d); } void Bot::rehash() { for (std::map >::iterator it = channelList->begin(); it != channelList->end(); ++it) serverConnection->queue->sendWho((*it).first); } String Bot::getUserhost(String channel, String nick) { Channel *c; if (channel == "") c = 0; else c = channelList->getChannel(channel); nick = nick.toLower(); if (c && c->hasNick(nick)) return c->getUser(nick)->userhost; unsigned long num = sentUserhostID++; serverConnection->queue->sendUserhost(nick); userhostMap[num] = "+"; while (userhostMap[num] == "+") { waitForInput(); serverConnection->queue->flush(); } // We have got our answer String res = userhostMap[num]; userhostMap.erase(num); return res; } bool Bot::iAmOp(String channel) { User * me = channelList->getChannel(channel)->getUser(nickName); return (me->mode & User::OP_MODE); } void Bot::init_user_functions () { // User Functions #define uf(f, l, b) new userFunction (f, l, b); userFunctions["ACTION"] = uf (UserCommands::Action, User::USER, true); userFunctions["ADDUSER"] = uf (UserCommands::AddUser, User::FRIEND, false); userFunctions["ADDSERVER"] = uf (UserCommands::AddServer, User::FRIEND, false); userFunctions["ADDSHIT"] = uf (UserCommands::AddShit, User::FRIEND, false); userFunctions["ALIAS"] = uf (UserCommands::Alias, User::MASTER, false); userFunctions["BAN"] = uf (UserCommands::Ban, User::USER, true); userFunctions["BANLIST"] = uf (UserCommands::BanList, User::USER, true); userFunctions["CHANNELS"] = uf (UserCommands::Channels, User::FRIEND, false); userFunctions["CYCLE"] = uf (UserCommands::Cycle, User::FRIEND, true); userFunctions["DCCLIST"] = uf (UserCommands::DCCList, User::FRIEND, false); userFunctions["DEBAN"] = uf (UserCommands::Deban, User::USER, true); userFunctions["DELSERVER"] = uf (UserCommands::DelServer, User::FRIEND, false); userFunctions["DELUSER"] = uf (UserCommands::DelUser, User::FRIEND, false); userFunctions["DELSHIT"] = uf (UserCommands::DelShit, User::FRIEND, false); userFunctions["DEOP"] = uf (UserCommands::Deop, User::TRUSTED_USER, true); userFunctions["DIE"] = uf (UserCommands::Die, User::MASTER, false); userFunctions["DO"] = uf (UserCommands::Do, User::MASTER, false); #ifdef USESCRIPTS userFunctions["EXECUTE"] = uf (UserCommands::Execute, User::MASTER, false); #endif userFunctions["HELP"] = uf (UserCommands::Help, User::NONE, false); userFunctions["IDENT"] = uf (UserCommands::Ident, User::NONE, true); userFunctions["INVITE"] = uf (UserCommands::Invite, User::USER, true); userFunctions["JOIN"] = uf (UserCommands::Join, User::FRIEND, false); userFunctions["KEEP"] = uf (UserCommands::Keep, User::FRIEND, true); userFunctions["KICK"] = uf (UserCommands::Kick, User::USER, true); userFunctions["KICKBAN"] = uf (UserCommands::KickBan, User::USER, true); userFunctions["LOAD"] = uf (UserCommands::Load, User::FRIEND, false); #ifdef USESCRIPTS userFunctions["LOADSCRIPT"] = uf (UserCommands::LoadScript, User::MASTER, false); #endif userFunctions["LOCK"] = uf (UserCommands::Lock, User::FRIEND, true); userFunctions["MODE"] = uf (UserCommands::Mode, User::FRIEND, true); userFunctions["MSG"] = uf (UserCommands::Msg, User::USER, false); userFunctions["NAMES"] = uf (UserCommands::Names, User::USER, true); userFunctions["NEXTSERVER"] = uf (UserCommands::NextServer, User::FRIEND, false); userFunctions["NICK"] = uf (UserCommands::Nick, User::FRIEND, false); userFunctions["NSLOOKUP"] = uf (UserCommands::NsLookup, User::USER, false); userFunctions["OP"] = uf (UserCommands::Op, User::TRUSTED_USER, true); userFunctions["PART"] = uf (UserCommands::Part, User::FRIEND, true); userFunctions["PASSWORD"] = uf (UserCommands::Password, User::USER, true); userFunctions["RECONNECT"] = uf (UserCommands::Reconnect, User::FRIEND, false); userFunctions["RSPYMESSAGE"] = uf (UserCommands::RSpyMessage, User::USER, false); userFunctions["SAVE"] = uf (UserCommands::Save, User::FRIEND, false); userFunctions["SAY"] = uf (UserCommands::Say, User::USER, true); userFunctions["SERVER"] = uf (UserCommands::Server, User::FRIEND, false); userFunctions["SERVERLIST"] = uf (UserCommands::ServerList, User::FRIEND, false); userFunctions["SETFLOODRATE"] = uf (UserCommands::SetFloodRate, User::MASTER, false); userFunctions["SETVERSION"] = uf (UserCommands::SetVersion, User::MASTER, false); userFunctions["SHITLIST"] = uf (UserCommands::ShitList, User::FRIEND, false); userFunctions["SPYLIST"] = uf (UserCommands::SpyList, User::USER, false); userFunctions["SPYMESSAGE"] = uf (UserCommands::SpyMessage, User::USER, false); userFunctions["STATS"] = uf (UserCommands::Stats, User::FRIEND, true); userFunctions["TBAN"] = uf (UserCommands::TBan, User::USER, true); userFunctions["TKBAN"] = uf (UserCommands::TKBan, User::USER, true); userFunctions["TOPIC"] = uf (UserCommands::Topic, User::USER, true); userFunctions["UNLOCK"] = uf (UserCommands::Unlock, User::FRIEND, true); userFunctions["USERLIST"] = uf (UserCommands::UserList, User::FRIEND, false); userFunctions["WHO"] = uf (UserCommands::Who, User::NONE, true); userFunctions["WHOIS"] = uf (UserCommands::Whois, User::FRIEND, true); #undef uf } namespace { void erase_userf (std::pair it) { delete it.second; } } void Bot::destroy_user_functions () { std::for_each (userFunctions.begin (), userFunctions.end (), erase_userf); userFunctions.erase (userFunctions.begin (), userFunctions.end ()); } void Bot::set_log_file (String name) { logFileName = logs_dir + name; logFile.close (); logFile.clear (); #if HAVE_IOSBASE logFile.open(logFileName.c_str (), std::ios_base::out | std::ios_base::ate | std::ios_base::app); #else logFile.open(logFileName.c_str (), ios::out | ios::ate | ios::app); #endif logLine("Starting log."); } void Bot::set_log_dir (String dir) { logs_dir = dir; DIR *temp = opendir (logs_dir.c_str ()); if (!temp) { mkdir (logs_dir.c_str (), S_IRWXU); } else { closedir (temp); } }