cb21075d |
1 | // Bot.C -*- C++ -*- |
2 | // Copyright (c) 1997, 1998 Etienne BERNARD |
3 | // Copyright (C) 2002 Clinton Ebadi |
4 | |
5 | // This program is free software; you can redistribute it and/or modify |
6 | // it under the terms of the GNU General Public License as published by |
7 | // the Free Software Foundation; either version 2 of the License, or |
8 | // any later version. |
9 | |
10 | // This program is distributed in the hope that it will be useful, |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | // GNU General Public License for more details. |
14 | |
15 | // You should have received a copy of the GNU General Public License |
16 | // along with this program; if not, write to the Free Software |
17 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. |
18 | |
19 | #include <fstream> |
20 | #include <iomanip> |
21 | #include <cstring> |
22 | #include <cstdlib> |
23 | #include <cstdio> |
24 | #include <sys/time.h> |
25 | #include <sys/types.h> |
26 | #include <unistd.h> |
27 | |
28 | #include "Bot.H" |
29 | #include "DCCConnection.H" |
30 | #include "StringTokenizer.H" |
31 | #include "ServerConnection.H" |
32 | #include "Utils.H" |
33 | |
34 | #define DEFAULT_NICKNAME "Bobot" |
35 | #define DEFAULT_USERNAME "bobot" |
36 | #define DEFAULT_IRCNAME "I'm a bobot++!" |
37 | #define DEFAULT_COMMANDCHAR '!' |
38 | #define DEFAULT_USERLISTFILENAME "bot.users" |
39 | #define DEFAULT_SHITLISTFILENAME "bot.shit" |
40 | #define DEFAULT_HELPFILENAME "bot.help" |
41 | #define DEFAULT_SCRIPTLOGFILENAME "script.log" |
42 | #define DEFAULT_LOGFILENAME "bot.log" |
439869bf |
43 | #define DEFAULT_LOGDIR getenv ("HOME") + String("/.bobotpp/logs/") |
cb21075d |
44 | #define DEFAULT_INITFILENAME "bot.init" |
45 | #ifdef USESCRIPTS |
46 | #define DEFAULT_AUTOEXECFILENAME "bot.autoexec" |
47 | #endif |
48 | |
49 | Bot::Bot(String filename, bool debug_on) |
50 | : nickName(DEFAULT_NICKNAME), |
51 | wantedNickName(DEFAULT_NICKNAME), |
52 | userName(DEFAULT_USERNAME), |
53 | ircName(DEFAULT_IRCNAME), |
54 | versionString(VERSION_STRING), |
55 | userHost(""), |
56 | localIP(""), |
57 | commandChar(DEFAULT_COMMANDCHAR), |
58 | configFileName(filename), |
59 | userListFileName(DEFAULT_USERLISTFILENAME), |
60 | shitListFileName(DEFAULT_SHITLISTFILENAME), |
61 | logFileName(DEFAULT_LOGFILENAME), |
439869bf |
62 | logs_dir (DEFAULT_LOGDIR), |
cb21075d |
63 | helpFileName(DEFAULT_HELPFILENAME), |
64 | initFileName(DEFAULT_INITFILENAME), |
65 | #ifdef USESCRIPTS |
66 | scriptLogFileName(DEFAULT_SCRIPTLOGFILENAME), |
67 | autoexecFileName(DEFAULT_AUTOEXECFILENAME), |
68 | #endif |
69 | connected(false), |
70 | debug(debug_on), stop(false), sentPing(false), |
71 | startTime(time(NULL)), currentTime(startTime), |
72 | lastNickNameChange(startTime), lastChannelJoin(startTime), |
73 | serverConnection(0), sentUserhostID(0), receivedUserhostID(0) |
74 | { |
75 | extern userFunctionsStruct userFunctionsInit[]; |
76 | |
77 | #ifdef HAVE_STL_CLEAR |
78 | wantedChannels.clear(); |
79 | ignoredUserhosts.clear(); |
80 | spyList.clear(); |
81 | userhostMap.clear(); |
82 | #endif |
83 | |
84 | for (int i = 0; userFunctionsInit[i].name[0] != '\0'; i++) { |
85 | userFunctions.push_back(new |
86 | userFunction(String(userFunctionsInit[i].name), |
87 | userFunctionsInit[i].function, |
88 | userFunctionsInit[i].minLevel, |
89 | userFunctionsInit[i].needsChannelName)); |
90 | } |
91 | |
92 | #if HAVE_IOSBASE |
439869bf |
93 | logFile.open(logs_dir + logFileName, std::ios_base::out | |
94 | std::ios_base::ate | std::ios_base::app); |
cb21075d |
95 | #else |
439869bf |
96 | logFile.open(logs_dir + logFileName, ios::out | ios::ate |
cb21075d |
97 | | ios::app); |
98 | #endif |
99 | logLine("Starting log."); |
100 | channelList = new ChannelList(); |
101 | serverList = new ServerList(); |
102 | readConfig(); |
103 | userList = new UserList(userListFileName); |
104 | shitList = new ShitList(shitListFileName); |
105 | todoList = new TodoList(); |
106 | |
107 | // Let's read the alias file |
108 | std::ifstream initFile(initFileName); |
109 | |
110 | if (initFile) { |
111 | String temp, alias, command; |
112 | std::list<userFunction *>::iterator it; |
113 | bool found = false; |
114 | userFunction *u; |
115 | int line = 0; |
116 | while (initFile >> temp, temp.length() != 0) { |
117 | line++; |
118 | StringTokenizer st(temp); |
119 | temp = temp.trim(); |
120 | if (temp[0]=='#') continue; |
121 | if (st.countTokens(' ') != 2) { |
122 | std::cerr << "Error when reading alias file (" << initFileName |
123 | << ") line " << line << "...\n"; |
124 | continue; |
125 | } |
126 | alias = st.nextToken().toUpper(); |
127 | command = st.nextToken().toUpper(); |
128 | |
129 | // Does the function already exist ? |
130 | found = false; |
131 | for (it = userFunctions.begin(); it != userFunctions.end(); ++it) |
132 | if (alias == (*it)->name) { |
133 | found = true; |
134 | break; |
135 | } |
136 | if (found) continue; |
137 | |
138 | // Check that the command exists |
139 | found = false; |
140 | for (it = userFunctions.begin(); it != userFunctions.end(); ++it) |
141 | if (command == (*it)->name) { |
142 | found = true; |
143 | u = *it; |
144 | break; |
145 | } |
146 | if (!found) continue; |
147 | |
148 | userFunctions.push_back (new |
149 | userFunction((char *)(const char *)alias, |
150 | u->function, |
151 | u->minLevel, |
152 | u->needsChannelName)); |
153 | } |
154 | } |
155 | |
156 | std::srand (std::time (0)); // srand for bot-random |
157 | #ifdef USESCRIPTS |
439869bf |
158 | botInterp = new BotInterp(this, logs_dir + scriptLogFileName); |
cb21075d |
159 | botInterp->LoadScript(autoexecFileName); |
160 | #endif |
161 | } |
162 | |
163 | Bot::~Bot() |
164 | { |
165 | // TODO: is it ok to delete iterators!?! |
166 | |
167 | Person *p; |
168 | while (spyList.size() != 0) { |
169 | p = (*spyList.begin()).second; |
170 | spyList.erase(spyList.begin()); |
171 | delete p; |
172 | } |
173 | DCCConnection *d; |
174 | while (dccConnections.size() != 0) { |
175 | d = *dccConnections.begin(); |
176 | dccConnections.erase(dccConnections.begin()); |
177 | delete d; |
178 | } |
179 | userFunction *u; |
180 | while (userFunctions.size() != 0) { |
181 | u = *userFunctions.begin(); |
182 | userFunctions.erase(userFunctions.begin()); |
183 | delete u; |
184 | } |
185 | wantedChannel *w; |
186 | while (wantedChannels.size() != 0) { |
187 | w = (*wantedChannels.begin()).second; |
188 | wantedChannels.erase(wantedChannels.begin()); |
189 | delete w; |
190 | } |
191 | userList->save(); |
192 | shitList->save(); |
193 | delete channelList; |
194 | delete userList; |
195 | delete todoList; |
196 | delete serverList; |
197 | delete shitList; |
198 | delete serverConnection; |
199 | logLine("Stopping log."); |
200 | logFile.close(); |
201 | } |
202 | |
203 | void |
204 | Bot::logLine(String line) |
205 | { |
206 | tm *d; |
207 | std::time_t current_time = time(0); |
208 | |
209 | d = localtime(¤t_time); |
210 | logFile << "[" << std::setfill('0') << std::setw(2) |
211 | << d->tm_mday << "/" << std::setfill('0') << std::setw(2) |
212 | << d->tm_mon + 1 << "/" |
213 | << d->tm_year + 1900 << " - " << std::setfill('0') << std::setw(2) |
214 | << d->tm_hour << ":" << std::setfill('0') << std::setw(2) |
215 | << d->tm_min << ":" << std::setfill('0') << std::setw(2) |
216 | << d->tm_sec << "] " |
217 | << line |
218 | << std::endl; |
219 | } |
220 | |
221 | void |
222 | Bot::readConfig() |
223 | { |
224 | std::ifstream file(configFileName); |
225 | String temp; |
226 | int line = 1; |
227 | |
228 | if (!file) { |
229 | logLine(String("I cannot find the file ") + configFileName); |
230 | return; |
231 | } |
232 | |
233 | while (!file.eof()) { |
234 | |
235 | file >> temp; |
236 | |
237 | if (temp.length() == 0 || temp[0] == '#') { |
238 | line++; |
239 | continue; |
240 | } |
241 | |
242 | StringTokenizer st(temp); |
243 | String command = st.nextToken('=').trim().toUpper(); |
244 | String parameters = st.nextToken('=').trim(); |
245 | |
246 | if (command == "NICK" || command == "NICKNAME") |
247 | nickName = wantedNickName = parameters; |
248 | else if (command == "USERNAME") |
249 | userName = parameters; |
250 | else if (command == "IRCNAME" || command == "REALNAME") |
251 | ircName = parameters; |
252 | else if (command == "CMDCHAR" || command == "COMMAND") |
253 | commandChar = parameters[0]; |
254 | else if (command == "USERLIST") |
255 | userListFileName = parameters; |
256 | else if (command == "SHITLIST") |
257 | shitListFileName = parameters; |
258 | else if (command == "CHANNEL") { |
259 | if (parameters.indexOf(':') == -1) { |
260 | std::cout << "Warning. The 'channel' syntax has changed." |
261 | << " Please see the README file for more information." |
262 | << " I will use compatibility mode, but you're really" |
263 | << " missing something.\n"; |
264 | StringTokenizer st2(parameters); |
265 | String name = st2.nextToken().toLower(); |
266 | String key = st2.nextToken(); |
267 | wantedChannels[name] = new wantedChannel("", "", key); |
268 | } else { |
269 | StringTokenizer st2(parameters); |
270 | String name = st2.nextToken(':').toLower(); |
271 | String mode = st2.nextToken(':'); |
272 | String keep = st2.nextToken(':'); |
273 | String key = st2.nextToken(':'); |
274 | wantedChannels[name] = new wantedChannel(mode, keep, key); |
275 | } |
276 | } |
277 | else if (command == "LOGFILE") |
439869bf |
278 | logFileName = parameters; |
cb21075d |
279 | #ifdef USESCRIPTS |
280 | else if (command == "SCRIPTLOGFILE") |
281 | scriptLogFileName = parameters; |
282 | else if (command == "AUTOEXECFILE") |
283 | autoexecFileName = parameters; |
284 | #endif |
285 | else if (command == "INITFILE") |
286 | initFileName = parameters; |
287 | else if (command == "LOCALIP") |
288 | localIP = parameters; |
289 | else if (command == "SERVER") { |
290 | if (parameters.indexOf(' ') == -1) |
291 | serverList->addServer(new Server(parameters)); |
292 | else { |
293 | StringTokenizer st2(parameters); |
294 | String name = st2.nextToken(); |
295 | int port = std::atoi(st2.nextToken()); |
296 | serverList->addServer(new Server(name, |
297 | port, |
298 | st2.nextToken())); |
299 | } |
300 | } |
301 | else { |
302 | logLine(String("Syntax error in file ") + configFileName + |
303 | ", line " + String((long)line)); |
304 | file.close(); |
305 | std::exit(1); |
306 | } |
307 | |
308 | line++; |
309 | } |
310 | |
311 | file.close(); |
312 | } |
313 | |
314 | void |
315 | Bot::run() |
316 | { |
317 | nextServer(); |
318 | |
319 | while (!stop) { |
320 | waitForInput(); // This is the main event loop |
321 | if (!serverConnection->queue->flush()) |
322 | nextServer(); |
323 | } |
324 | } |
325 | |
326 | void |
327 | Bot::waitForInput() |
328 | { |
329 | #ifdef _HPUX_SOURCE |
330 | int rd; |
331 | #else |
332 | fd_set rd; |
333 | #endif |
334 | struct timeval timer; |
335 | |
336 | int sock = serverConnection->getFileDescriptor(); |
337 | int maxSocketNumber = sock; |
338 | |
339 | #ifdef _HPUX_SOURCE |
340 | rd = sock; |
341 | #else |
342 | FD_ZERO(&rd); |
343 | FD_SET(sock, &rd); |
344 | #endif |
345 | |
346 | for (std::list<DCCConnection *>::iterator it = dccConnections.begin(); |
347 | it != dccConnections.end(); ++it) { |
348 | int s = (*it)->getFileDescriptor(); |
349 | #ifdef _HPUX_SOURCE |
350 | rd |= s; |
351 | #else |
352 | FD_SET(s, &rd); |
353 | #endif |
354 | if (s > maxSocketNumber) |
355 | maxSocketNumber = s; |
356 | } |
357 | |
358 | timer.tv_sec = 1; |
359 | timer.tv_usec = 0; |
360 | |
361 | switch (select(maxSocketNumber + 1, &rd, NULL, NULL, &timer)) { |
362 | case 0: /* timeout */ |
363 | break; |
364 | case -1: /* error */ |
365 | break; |
366 | default: /* normal */ |
367 | #ifdef _HPUX_SOURCE |
368 | if (rd & sock) |
369 | #else |
370 | if (FD_ISSET(sock, &rd)) |
371 | #endif |
372 | if (serverConnection->handleInput()) |
373 | nextServer(); |
374 | |
375 | std::list<DCCConnection *>::iterator it = dccConnections.begin(); |
376 | std::list<DCCConnection *>::iterator it2; |
377 | |
378 | while (it != dccConnections.end()) { |
379 | it2 = it; |
380 | ++it; |
381 | #ifdef _HPUX_SOURCE |
382 | if (rd & (*it2)->getFileDescriptor()) { |
383 | #else |
384 | if (FD_ISSET((*it2)->getFileDescriptor(), &rd)) { |
385 | #endif |
386 | if ((*it2)->handleInput()) { |
387 | delete *it2; |
388 | dccConnections.erase(it2); |
389 | } |
390 | } |
391 | } |
392 | } |
393 | |
394 | if (currentTime < std::time(NULL)) { // Actions that we do each second |
395 | currentTime = std::time(NULL); |
396 | for (std::map<String, unsigned int, std::less<String> >::iterator |
397 | it = ignoredUserhosts.begin(); |
398 | it != ignoredUserhosts.end(); ++it) |
399 | if ((*it).second > 0) |
400 | (*it).second--; |
401 | |
402 | String line; |
403 | while ((line = todoList->getNext()) != "") { |
404 | serverConnection->queue->sendChannelMode(line); |
405 | } |
406 | #ifdef USESCRIPTS |
407 | botInterp->RunTimers(currentTime); |
408 | #endif |
409 | |
410 | #ifdef USESCRIPTS |
411 | tm *thisTime = localtime(¤tTime); |
412 | if (thisTime->tm_sec == 0) { |
413 | char s[6]; |
414 | sprintf(s, "%2d:%2d", thisTime->tm_hour, thisTime->tm_min); |
415 | botInterp->RunHooks(Hook::TIMER, String(s), |
416 | gh_list(Utils::string2SCM(String(s)), SCM_UNDEFINED)); |
417 | } |
418 | #endif |
419 | |
420 | } |
421 | |
422 | if (currentTime >= (time_t)(lastNickNameChange + Bot::NICK_CHANGE) && |
423 | nickName != wantedNickName) { |
424 | lastNickNameChange = currentTime; |
425 | serverConnection->queue->sendNick(wantedNickName); |
426 | } |
427 | |
428 | if (currentTime >= (std::time_t)(lastChannelJoin + Bot::CHANNEL_JOIN)) { |
429 | lastChannelJoin = currentTime; |
430 | for (std::map<String, wantedChannel *, std::less<String> >::iterator it = |
431 | wantedChannels.begin(); it != wantedChannels.end(); |
432 | ++it) |
433 | if (channelList->getChannel((*it).first) == 0) |
434 | serverConnection->queue->sendJoin((*it).first, (*it).second->key); |
435 | } |
436 | |
437 | std::list<DCCConnection *>::iterator it2; |
438 | |
439 | for (std::list<DCCConnection *>::iterator it = dccConnections.begin(); |
440 | it != dccConnections.end(); ) { |
441 | it2 = it; |
442 | ++it; |
443 | if ((*it2)->autoRemove && currentTime >= (std::time_t)((*it2)->lastSpoken + Bot::DCC_DELAY)) { |
444 | delete *it2; |
445 | dccConnections.erase(it2); |
446 | } |
447 | } |
448 | |
449 | if (currentTime >= (std::time_t)(serverConnection->serverLastSpoken + Bot::PING_TIME) && !sentPing) { |
450 | serverConnection->queue->sendPing("Testing connection"); |
451 | sentPing = true; |
452 | } |
453 | |
454 | if (currentTime >= (std::time_t)(serverConnection->serverLastSpoken + Bot::TIMEOUT)) { |
455 | sentPing = false; |
456 | nextServer(); |
457 | } |
458 | } |
459 | |
460 | // We can change server if we will not lose op on a channel |
461 | bool |
462 | Bot::canChangeServer() |
463 | { |
464 | String channel; |
465 | Channel *c; |
466 | |
467 | for (std::map<String, Channel *, std::less<String> >::iterator it = |
468 | channelList->begin(); |
469 | it != channelList->end(); ++it) { |
470 | channel = (*it).first; |
471 | c = channelList->getChannel(channel); |
472 | if (c->countOp == 1 && |
473 | c->count > 1 && this->iAmOp(channel)) |
474 | return false; |
475 | } |
476 | return true; |
477 | } |
478 | |
479 | void |
480 | Bot::nextServer() |
481 | { |
482 | bool cont = false; |
483 | |
484 | if (channelList) |
485 | channelList->clear(); |
486 | |
487 | if (serverConnection) |
488 | userList->removeFirst(); |
489 | |
490 | delete serverConnection; |
491 | |
492 | do { |
493 | Server * s = serverList->nextServer(); |
494 | if (!s) { |
495 | std::cout << "No server found. Exiting..." << std::endl; |
496 | std::exit(1); |
497 | } |
498 | serverConnection = new ServerConnection(this, s, localIP); |
499 | if (!serverConnection->connect()) { |
500 | cont = true; |
501 | // We sleep 10 seconds, to avoid connection flood |
502 | sleep(10); |
503 | delete serverConnection; |
504 | } else { |
505 | cont = false; |
506 | } |
507 | } while (cont); |
508 | } |
509 | |
510 | void |
511 | Bot::reconnect() |
512 | { |
513 | if (channelList) |
514 | channelList->clear(); |
515 | |
516 | userList->removeFirst(); |
517 | |
518 | delete serverConnection; |
519 | |
520 | serverConnection = |
521 | new ServerConnection(this, serverList->currentServer(), localIP); |
522 | |
523 | serverConnection->connect(); |
524 | } |
525 | |
526 | void |
527 | Bot::connect(int serverNumber) |
528 | { |
529 | if (channelList) |
530 | channelList->clear(); |
531 | |
532 | userList->removeFirst(); |
533 | |
534 | delete serverConnection; |
535 | |
536 | serverConnection = |
537 | new ServerConnection(this, serverList->get(serverNumber), localIP); |
538 | |
539 | serverConnection->connect(); |
540 | } |
541 | |
542 | void |
543 | Bot::addDCC(Person * from, unsigned long address, int port) |
544 | { |
545 | DCCConnection * d = new DCCConnection(this, from->getAddress(), |
546 | address, port); |
547 | |
548 | if (!d->connect()) |
549 | return; |
550 | |
551 | dccConnections.push_back(d); |
552 | } |
553 | |
554 | void |
555 | Bot::rehash() |
556 | { |
557 | for (std::map<String, Channel *, std::less<String> >::iterator it = channelList->begin(); |
558 | it != channelList->end(); ++it) |
559 | serverConnection->queue->sendWho((*it).first); |
560 | } |
561 | |
562 | String |
563 | Bot::getUserhost(String channel, String nick) |
564 | { |
565 | Channel *c; |
566 | |
567 | if (channel == "") |
568 | c = 0; |
569 | else |
570 | c = channelList->getChannel(channel); |
571 | |
572 | nick = nick.toLower(); |
573 | |
574 | |
575 | if (c && c->hasNick(nick)) |
576 | return c->getUser(nick)->userhost; |
577 | |
578 | unsigned long num = sentUserhostID++; |
579 | |
580 | serverConnection->queue->sendUserhost(nick); |
581 | userhostMap[num] = "+"; |
582 | |
583 | while (userhostMap[num] == "+") { |
584 | waitForInput(); |
585 | serverConnection->queue->flush(); |
586 | } |
587 | |
588 | // We have got our answer |
589 | String res = userhostMap[num]; |
590 | userhostMap.erase(num); |
591 | |
592 | return res; |
593 | } |
594 | |
595 | bool |
596 | Bot::iAmOp(String channel) |
597 | { |
598 | User * me = channelList->getChannel(channel)->getUser(nickName); |
599 | return (me->mode & User::OP_MODE); |
600 | } |