| OLD | NEW |
| (Empty) |
| 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@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@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 | |
| OLD | NEW |