1 # Copyright (C) 1999, 2000 Joel Rosdahl
\r
3 # This program is free software; you can redistribute it and/or
\r
4 # modify it under the terms of the GNU General Public License
\r
5 # as published by the Free Software Foundation; either version 2
\r
6 # of the License, or (at your option) any later version.
\r
8 # This program is distributed in the hope that it will be useful,
\r
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
\r
11 # GNU General Public License for more details.
\r
13 # You should have received a copy of the GNU General Public License
\r
14 # along with this program; if not, write to the Free Software
\r
15 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
\r
17 # Joel Rosdahl <joel@rosdahl.net>
\r
19 # $Id: ircbot.py,v 1.1 2002/02/07 09:15:32 matthew Exp $
\r
21 """ircbot -- Simple IRC bot library.
\r
23 This module contains a single-server IRC bot class that can be used to
\r
29 from UserDict import UserDict
\r
31 from irclib import SimpleIRCClient
\r
32 from irclib import nm_to_n, irc_lower, all_events
\r
33 from irclib import parse_channel_modes, is_channel, is_channel
\r
34 from irclib import ServerConnectionError
\r
36 class SingleServerIRCBot(SimpleIRCClient):
\r
37 """A single-server IRC bot class.
\r
39 The bot tries to reconnect if it is disconnected.
\r
41 The bot keeps track of the channels it has joined, the other
\r
42 clients that are present in the channels and which of those that
\r
43 have operator or voice modes. The "database" is kept in the
\r
44 self.channels attribute, which is an IRCDict of Channels.
\r
46 def __init__(self, server_list, nickname, realname, reconnection_interval=60):
\r
47 """Constructor for SingleServerIRCBot objects.
\r
51 server_list -- A list of tuples (server, port) that
\r
52 defines which servers the bot should try to
\r
55 nickname -- The bot's nickname.
\r
57 realname -- The bot's realname.
\r
59 reconnection_interval -- How long the bot should wait
\r
60 before trying to reconnect.
\r
63 SimpleIRCClient.__init__(self)
\r
64 self.channels = IRCDict()
\r
65 self.server_list = server_list
\r
66 if not reconnection_interval or reconnection_interval < 0:
\r
67 reconnection_interval = 2**31
\r
68 self.reconnection_interval = reconnection_interval
\r
70 self._nickname = nickname
\r
71 self._realname = realname
\r
72 for i in ["disconnect", "join", "kick", "mode",
\r
73 "namreply", "nick", "part", "quit"]:
\r
74 self.connection.add_global_handler(i,
\r
75 getattr(self, "_on_" + i),
\r
77 def _connected_checker(self):
\r
79 if not self.connection.is_connected():
\r
80 self.connection.execute_delayed(self.reconnection_interval,
\r
81 self._connected_checker)
\r
87 if len(self.server_list[0]) > 2:
\r
88 password = self.server_list[0][2]
\r
90 self.connect(self.server_list[0][0],
\r
91 self.server_list[0][1],
\r
94 ircname=self._realname)
\r
95 except ServerConnectionError:
\r
98 def _on_disconnect(self, c, e):
\r
100 self.channels = IRCDict()
\r
101 self.connection.execute_delayed(self.reconnection_interval,
\r
102 self._connected_checker)
\r
104 def _on_join(self, c, e):
\r
107 nick = nm_to_n(e.source())
\r
108 if nick == self._nickname:
\r
109 self.channels[ch] = Channel()
\r
110 self.channels[ch].add_user(nick)
\r
112 def _on_kick(self, c, e):
\r
114 nick = e.arguments()[0]
\r
115 channel = e.target()
\r
117 if nick == self._nickname:
\r
118 del self.channels[channel]
\r
120 self.channels[channel].remove_user(nick)
\r
122 def _on_mode(self, c, e):
\r
124 modes = parse_channel_modes(string.join(e.arguments()))
\r
127 ch = self.channels[t]
\r
133 f(mode[1], mode[2])
\r
135 # Mode on self... XXX
\r
138 def _on_namreply(self, c, e):
\r
141 # e.arguments()[0] == "=" (why?)
\r
142 # e.arguments()[1] == channel
\r
143 # e.arguments()[2] == nick list
\r
145 ch = e.arguments()[1]
\r
146 for nick in string.split(e.arguments()[2]):
\r
149 self.channels[ch].set_mode("o", nick)
\r
150 elif nick[0] == "+":
\r
152 self.channels[ch].set_mode("v", nick)
\r
153 self.channels[ch].add_user(nick)
\r
155 def _on_nick(self, c, e):
\r
157 before = nm_to_n(e.source())
\r
159 for ch in self.channels.values():
\r
160 if ch.has_user(before):
\r
161 ch.change_nick(before, after)
\r
162 if nm_to_n(before) == self._nickname:
\r
163 self._nickname = after
\r
165 def _on_part(self, c, e):
\r
167 nick = nm_to_n(e.source())
\r
168 channel = e.target()
\r
170 if nick == self._nickname:
\r
171 del self.channels[channel]
\r
173 self.channels[channel].remove_user(nick)
\r
175 def _on_quit(self, c, e):
\r
177 nick = nm_to_n(e.source())
\r
178 for ch in self.channels.values():
\r
179 if ch.has_user(nick):
\r
180 ch.remove_user(nick)
\r
182 def die(self, msg="Bye, cruel world!"):
\r
183 """Let the bot die.
\r
187 msg -- Quit message.
\r
189 self.connection.quit(msg)
\r
192 def disconnect(self, msg="I'll be back!"):
\r
193 """Disconnect the bot.
\r
195 The bot will try to reconnect after a while.
\r
199 msg -- Quit message.
\r
201 self.connection.quit(msg)
\r
203 def get_version(self):
\r
204 """Returns the bot version.
\r
206 Used when answering a CTCP VERSION request.
\r
208 return "VERSION ircbot.py by Joel Rosdahl <joel@rosdahl.net>, Matthew Vernon <matthew@debian.org> and others"
\r
210 def jump_server(self):
\r
211 """Connect to a new server, possible disconnecting from the current.
\r
213 The bot will skip to next server in the server_list each time
\r
214 jump_server is called.
\r
216 if self.connection.is_connected():
\r
217 self.connection.quit("Jumping servers")
\r
218 self.server_list.append(self.server_list.pop(0))
\r
221 def on_ctcp(self, c, e):
\r
222 """Default handler for ctcp events.
\r
224 Replies to VERSION and PING requests.
\r
226 if e.arguments()[0] == "VERSION":
\r
227 c.ctcp_reply(nm_to_n(e.source()), self.get_version())
\r
228 elif e.arguments()[0] == "PING":
\r
229 if len(e.arguments()) > 1:
\r
230 c.ctcp_reply(nm_to_n(e.source()),
\r
231 "PING " + e.arguments()[1])
\r
234 """Start the bot."""
\r
236 SimpleIRCClient.start(self)
\r
240 """A dictionary suitable for storing IRC-related things.
\r
242 Dictionary keys a and b are considered equal if and only if
\r
243 irc_lower(a) == irc_lower(b)
\r
245 Otherwise, it should behave exactly as a normal dictionary.
\r
248 def __init__(self, dict=None):
\r
250 self.canon_keys = {} # Canonical keys
\r
251 if dict is not None:
\r
253 def __repr__(self):
\r
254 return repr(self.data)
\r
255 def __cmp__(self, dict):
\r
256 if isinstance(dict, IRCDict):
\r
257 return cmp(self.data, dict.data)
\r
259 return cmp(self.data, dict)
\r
261 return len(self.data)
\r
262 def __getitem__(self, key):
\r
263 return self.data[self.canon_keys[irc_lower(key)]]
\r
264 def __setitem__(self, key, item):
\r
265 if self.has_key(key):
\r
267 self.data[key] = item
\r
268 self.canon_keys[irc_lower(key)] = key
\r
269 def __delitem__(self, key):
\r
270 ck = irc_lower(key)
\r
271 del self.data[self.canon_keys[ck]]
\r
272 del self.canon_keys[ck]
\r
275 self.canon_keys.clear()
\r
277 if self.__class__ is UserDict:
\r
278 return UserDict(self.data)
\r
280 return copy.copy(self)
\r
282 return self.data.keys()
\r
284 return self.data.items()
\r
286 return self.data.values()
\r
287 def has_key(self, key):
\r
288 return self.canon_keys.has_key(irc_lower(key))
\r
289 def update(self, dict):
\r
290 for k, v in dict.items():
\r
292 def get(self, key, failobj=None):
\r
293 return self.data.get(key, failobj)
\r
297 """A class for keeping information about an IRC channel.
\r
299 This class can be improved a lot.
\r
302 def __init__(self):
\r
303 self.userdict = IRCDict()
\r
304 self.operdict = IRCDict()
\r
305 self.voiceddict = IRCDict()
\r
309 """Returns an unsorted list of the channel's users."""
\r
310 return self.userdict.keys()
\r
313 """Returns an unsorted list of the channel's operators."""
\r
314 return self.operdict.keys()
\r
317 """Returns an unsorted list of the persons that have voice
\r
318 mode set in the channel."""
\r
319 return self.voiceddict.keys()
\r
321 def has_user(self, nick):
\r
322 """Check whether the channel has a user."""
\r
323 return self.userdict.has_key(nick)
\r
325 def is_oper(self, nick):
\r
326 """Check whether a user has operator status in the channel."""
\r
327 return self.operdict.has_key(nick)
\r
329 def is_voiced(self, nick):
\r
330 """Check whether a user has voice mode set in the channel."""
\r
331 return self.voiceddict.has_key(nick)
\r
333 def add_user(self, nick):
\r
334 self.userdict[nick] = 1
\r
336 def remove_user(self, nick):
\r
337 for d in self.userdict, self.operdict, self.voiceddict:
\r
338 if d.has_key(nick):
\r
341 def change_nick(self, before, after):
\r
342 self.userdict[after] = 1
\r
343 del self.userdict[before]
\r
344 if self.operdict.has_key(before):
\r
345 self.operdict[after] = 1
\r
346 del self.operdict[before]
\r
347 if self.voiceddict.has_key(before):
\r
348 self.voiceddict[after] = 1
\r
349 del self.voiceddict[before]
\r
351 def set_mode(self, mode, value=None):
\r
352 """Set mode on the channel.
\r
356 mode -- The mode (a single-character string).
\r
361 self.operdict[value] = 1
\r
363 self.voiceddict[value] = 1
\r
365 self.modes[mode] = value
\r
367 def clear_mode(self, mode, value=None):
\r
368 """Clear mode on the channel.
\r
372 mode -- The mode (a single-character string).
\r
378 del self.operdict[value]
\r
380 del self.voiceddict[value]
\r
382 del self.modes[mode]
\r
386 def has_mode(self, mode):
\r
387 return mode in self.modes
\r
389 def is_moderated(self):
\r
390 return self.has_mode("m")
\r
392 def is_secret(self):
\r
393 return self.has_mode("s")
\r
395 def is_protected(self):
\r
396 return self.has_mode("p")
\r
398 def has_topic_lock(self):
\r
399 return self.has_mode("t")
\r
401 def is_invite_only(self):
\r
402 return self.has_mode("i")
\r
404 def has_message_from_outside_protection(self):
\r
405 # Eh... What should it be called, really?
\r
406 return self.has_mode("n")
\r
408 def has_limit(self):
\r
409 return self.has_mode("l")
\r
412 if self.has_limit():
\r
413 return self.modes[l]
\r
418 return self.has_mode("k")
\r
422 return self.modes["k"]
\r