1 # Copyright (C) 1999--2002 Joel Rosdahl 2 # 3 # This library is free software; you can redistribute it and/or 4 # modify it under the terms of the GNU Lesser General Public 5 # License as published by the Free Software Foundation; either 6 # version 2.1 of the License, or (at your option) any later version. 7 # 8 # This library is distributed in the hope that it will be useful, 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 # Lesser General Public License for more details. 12 # 13 # You should have received a copy of the GNU Lesser General Public 14 # License along with this library; if not, write to the Free Software 15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 16 # 17 # Joel Rosdahl <joel (at] rosdahl.net> 18 # 19 # $Id: ircbot.py,v 1.23 2008/09/11 07:38:30 keltus Exp $ 20 21 """ircbot -- Simple IRC bot library. 22 23 This module contains a single-server IRC bot class that can be used to 24 write simpler bots. 25 """ 26 27 import sys 28 from UserDict import UserDict 29 30 from irclib import SimpleIRCClient 31 from irclib import nm_to_n, irc_lower, all_events 32 from irclib import parse_channel_modes, is_channel 33 from irclib import ServerConnectionError 34 35 class SingleServerIRCBot(SimpleIRCClient): 36 """A single-server IRC bot class. 37 38 The bot tries to reconnect if it is disconnected. 39 40 The bot keeps track of the channels it has joined, the other 41 clients that are present in the channels and which of those that 42 have operator or voice modes. The "database" is kept in the 43 self.channels attribute, which is an IRCDict of Channels. 44 """ 45 def __init__(self, server_list, nickname, realname, reconnection_interval=60): 46 """Constructor for SingleServerIRCBot objects. 47 48 Arguments: 49 50 server_list -- A list of tuples (server, port) that 51 defines which servers the bot should try to 52 connect to. 53 54 nickname -- The bot's nickname. 55 56 realname -- The bot's realname. 57 58 reconnection_interval -- How long the bot should wait 59 before trying to reconnect. 60 61 dcc_connections -- A list of initiated/accepted DCC 62 connections. 63 """ 64 65 SimpleIRCClient.__init__(self) 66 self.channels = IRCDict() 67 self.server_list = server_list 68 if not reconnection_interval or reconnection_interval < 0: 69 reconnection_interval = 2**31 70 self.reconnection_interval = reconnection_interval 71 72 self._nickname = nickname 73 self._realname = realname 74 for i in ["disconnect", "join", "kick", "mode", 75 "namreply", "nick", "part", "quit"]: 76 self.connection.add_global_handler(i, 77 getattr(self, "_on_" + i), 78 -10) 79 def _connected_checker(self): 80 """[Internal]""" 81 if not self.connection.is_connected(): 82 self.connection.execute_delayed(self.reconnection_interval, 83 self._connected_checker) 84 self.jump_server() 85 86 def _connect(self): 87 """[Internal]""" 88 password = None 89 if len(self.server_list[0]) > 2: 90 password = self.server_list[0][2] 91 try: 92 self.connect(self.server_list[0][0], 93 self.server_list[0][1], 94 self._nickname, 95 password, 96 ircname=self._realname) 97 except ServerConnectionError: 98 pass 99 100 def _on_disconnect(self, c, e): 101 """[Internal]""" 102 self.channels = IRCDict() 103 self.connection.execute_delayed(self.reconnection_interval, 104 self._connected_checker) 105 106 def _on_join(self, c, e): 107 """[Internal]""" 108 ch = e.target() 109 nick = nm_to_n(e.source()) 110 if nick == c.get_nickname(): 111 self.channels[ch] = Channel() 112 self.channels[ch].add_user(nick) 113 114 def _on_kick(self, c, e): 115 """[Internal]""" 116 nick = e.arguments()[0] 117 channel = e.target() 118 119 if nick == c.get_nickname(): 120 del self.channels[channel] 121 else: 122 self.channels[channel].remove_user(nick) 123 124 def _on_mode(self, c, e): 125 """[Internal]""" 126 modes = parse_channel_modes(" ".join(e.arguments())) 127 t = e.target() 128 if is_channel(t): 129 ch = self.channels[t] 130 for mode in modes: 131 if mode[0] == "+": 132 f = ch.set_mode 133 else: 134 f = ch.clear_mode 135 f(mode[1], mode[2]) 136 else: 137 # Mode on self... XXX 138 pass 139 140 def _on_namreply(self, c, e): 141 """[Internal]""" 142 143 # e.arguments()[0] == "@" for secret channels, 144 # "*" for private channels, 145 # "=" for others (public channels) 146 # e.arguments()[1] == channel 147 # e.arguments()[2] == nick list 148 149 ch = e.arguments()[1] 150 for nick in e.arguments()[2].split(): 151 if nick[0] == "@": 152 nick = nick[1:] 153 self.channels[ch].set_mode("o", nick) 154 elif nick[0] == "+": 155 nick = nick[1:] 156 self.channels[ch].set_mode("v", nick) 157 self.channels[ch].add_user(nick) 158 159 def _on_nick(self, c, e): 160 """[Internal]""" 161 before = nm_to_n(e.source()) 162 after = e.target() 163 for ch in self.channels.values(): 164 if ch.has_user(before): 165 ch.change_nick(before, after) 166 167 def _on_part(self, c, e): 168 """[Internal]""" 169 nick = nm_to_n(e.source()) 170 channel = e.target() 171 172 if nick == c.get_nickname(): 173 del self.channels[channel] 174 else: 175 self.channels[channel].remove_user(nick) 176 177 def _on_quit(self, c, e): 178 """[Internal]""" 179 nick = nm_to_n(e.source()) 180 for ch in self.channels.values(): 181 if ch.has_user(nick): 182 ch.remove_user(nick) 183 184 def die(self, msg="Bye, cruel world!"): 185 """Let the bot die. 186 187 Arguments: 188 189 msg -- Quit message. 190 """ 191 192 self.connection.disconnect(msg) 193 sys.exit(0) 194 195 def disconnect(self, msg="I'll be back!"): 196 """Disconnect the bot. 197 198 The bot will try to reconnect after a while. 199 200 Arguments: 201 202 msg -- Quit message. 203 """ 204 self.connection.disconnect(msg) 205 206 def get_version(self): 207 """Returns the bot version. 208 209 Used when answering a CTCP VERSION request. 210 """ 211 return "ircbot.py by Joel Rosdahl <joel (at] rosdahl.net>" 212 213 def jump_server(self, msg="Changing servers"): 214 """Connect to a new server, possibly disconnecting from the current. 215 216 The bot will skip to next server in the server_list each time 217 jump_server is called. 218 """ 219 if self.connection.is_connected(): 220 self.connection.disconnect(msg) 221 222 self.server_list.append(self.server_list.pop(0)) 223 self._connect() 224 225 def on_ctcp(self, c, e): 226 """Default handler for ctcp events. 227 228 Replies to VERSION and PING requests and relays DCC requests 229 to the on_dccchat method. 230 """ 231 if e.arguments()[0] == "VERSION": 232 c.ctcp_reply(nm_to_n(e.source()), 233 "VERSION " + self.get_version()) 234 elif e.arguments()[0] == "PING": 235 if len(e.arguments()) > 1: 236 c.ctcp_reply(nm_to_n(e.source()), 237 "PING " + e.arguments()[1]) 238 elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT": 239 self.on_dccchat(c, e) 240 241 def on_dccchat(self, c, e): 242 pass 243 244 def start(self): 245 """Start the bot.""" 246 self._connect() 247 SimpleIRCClient.start(self) 248 249 250 class IRCDict: 251 """A dictionary suitable for storing IRC-related things. 252 253 Dictionary keys a and b are considered equal if and only if 254 irc_lower(a) == irc_lower(b) 255 256 Otherwise, it should behave exactly as a normal dictionary. 257 """ 258 259 def __init__(self, dict=None): 260 self.data = {} 261 self.canon_keys = {} # Canonical keys 262 if dict is not None: 263 self.update(dict) 264 def __repr__(self): 265 return repr(self.data) 266 def __cmp__(self, dict): 267 if isinstance(dict, IRCDict): 268 return cmp(self.data, dict.data) 269 else: 270 return cmp(self.data, dict) 271 def __len__(self): 272 return len(self.data) 273 def __getitem__(self, key): 274 return self.data[self.canon_keys[irc_lower(key)]] 275 def __setitem__(self, key, item): 276 if key in self: 277 del self[key] 278 self.data[key] = item 279 self.canon_keys[irc_lower(key)] = key 280 def __delitem__(self, key): 281 ck = irc_lower(key) 282 del self.data[self.canon_keys[ck]] 283 del self.canon_keys[ck] 284 def __iter__(self): 285 return iter(self.data) 286 def __contains__(self, key): 287 return self.has_key(key) 288 def clear(self): 289 self.data.clear() 290 self.canon_keys.clear() 291 def copy(self): 292 if self.__class__ is UserDict: 293 return UserDict(self.data) 294 import copy 295 return copy.copy(self) 296 def keys(self): 297 return self.data.keys() 298 def items(self): 299 return self.data.items() 300 def values(self): 301 return self.data.values() 302 def has_key(self, key): 303 return irc_lower(key) in self.canon_keys 304 def update(self, dict): 305 for k, v in dict.items(): 306 self.data[k] = v 307 def get(self, key, failobj=None): 308 return self.data.get(key, failobj) 309 310 311 class Channel: 312 """A class for keeping information about an IRC channel. 313 314 This class can be improved a lot. 315 """ 316 317 def __init__(self): 318 self.userdict = IRCDict() 319 self.operdict = IRCDict() 320 self.voiceddict = IRCDict() 321 self.modes = {} 322 323 def users(self): 324 """Returns an unsorted list of the channel's users.""" 325 return self.userdict.keys() 326 327 def opers(self): 328 """Returns an unsorted list of the channel's operators.""" 329 return self.operdict.keys() 330 331 def voiced(self): 332 """Returns an unsorted list of the persons that have voice 333 mode set in the channel.""" 334 return self.voiceddict.keys() 335 336 def has_user(self, nick): 337 """Check whether the channel has a user.""" 338 return nick in self.userdict 339 340 def is_oper(self, nick): 341 """Check whether a user has operator status in the channel.""" 342 return nick in self.operdict 343 344 def is_voiced(self, nick): 345 """Check whether a user has voice mode set in the channel.""" 346 return nick in self.voiceddict 347 348 def add_user(self, nick): 349 self.userdict[nick] = 1 350 351 def remove_user(self, nick): 352 for d in self.userdict, self.operdict, self.voiceddict: 353 if nick in d: 354 del d[nick] 355 356 def change_nick(self, before, after): 357 self.userdict[after] = 1 358 del self.userdict[before] 359 if before in self.operdict: 360 self.operdict[after] = 1 361 del self.operdict[before] 362 if before in self.voiceddict: 363 self.voiceddict[after] = 1 364 del self.voiceddict[before] 365 366 def set_mode(self, mode, value=None): 367 """Set mode on the channel. 368 369 Arguments: 370 371 mode -- The mode (a single-character string). 372 373 value -- Value 374 """ 375 if mode == "o": 376 self.operdict[value] = 1 377 elif mode == "v": 378 self.voiceddict[value] = 1 379 else: 380 self.modes[mode] = value 381 382 def clear_mode(self, mode, value=None): 383 """Clear mode on the channel. 384 385 Arguments: 386 387 mode -- The mode (a single-character string). 388 389 value -- Value 390 """ 391 try: 392 if mode == "o": 393 del self.operdict[value] 394 elif mode == "v": 395 del self.voiceddict[value] 396 else: 397 del self.modes[mode] 398 except KeyError: 399 pass 400 401 def has_mode(self, mode): 402 return mode in self.modes 403 404 def is_moderated(self): 405 return self.has_mode("m") 406 407 def is_secret(self): 408 return self.has_mode("s") 409 410 def is_protected(self): 411 return self.has_mode("p") 412 413 def has_topic_lock(self): 414 return self.has_mode("t") 415 416 def is_invite_only(self): 417 return self.has_mode("i") 418 419 def has_allow_external_messages(self): 420 return self.has_mode("n") 421 422 def has_limit(self): 423 return self.has_mode("l") 424 425 def limit(self): 426 if self.has_limit(): 427 return self.modes[l] 428 else: 429 return None 430 431 def has_key(self): 432 return self.has_mode("k") 433 434 def key(self): 435 if self.has_key(): 436 return self.modes["k"] 437 else: 438 return None 439