Caught signals that would otherwise be fatal and saveless.
[halbot.git] / ircbot.py
blobcb2b01d0d80505bcb67165669a3593b1b97cc3c3
1 # Copyright (C) 1999--2002 Joel Rosdahl
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.
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.
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
17 # Joel Rosdahl <joel@rosdahl.net>
19 # $Id: ircbot.py,v 1.21 2005/12/23 18:44:43 keltus Exp $
21 """ircbot -- Simple IRC bot library.
23 This module contains a single-server IRC bot class that can be used to
24 write simpler bots.
25 """
27 import sys
28 from UserDict import UserDict
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
35 class SingleServerIRCBot(SimpleIRCClient):
36 """A single-server IRC bot class.
38 The bot tries to reconnect if it is disconnected.
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.
48 Arguments:
50 server_list -- A list of tuples (server, port) that
51 defines which servers the bot should try to
52 connect to.
54 nickname -- The bot's nickname.
56 realname -- The bot's realname.
58 reconnection_interval -- How long the bot should wait
59 before trying to reconnect.
61 dcc_connections -- A list of initiated/accepted DCC
62 connections.
63 """
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
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()
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
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)
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(ch)
112 self.channels[ch].add_user(nick)
114 def _on_kick(self, c, e):
115 """[Internal]"""
116 nick = e.arguments()[0]
117 channel = e.target()
119 if nick == c.get_nickname():
120 del self.channels[channel]
121 else:
122 self.channels[channel].remove_user(nick)
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], self.connection)
136 else:
137 # Mode on self... XXX
138 pass
140 nick_symbols = {"~": "q", "&": "a", "@": "o", "%": "h", "+": "v"}
141 def _on_namreply(self, c, e):
142 """[Internal]"""
144 # e.arguments()[0] == "@" for secret channels,
145 # "*" for private channels,
146 # "=" for others (public channels)
147 # e.arguments()[1] == channel
148 # e.arguments()[2] == nick list
150 ch = e.arguments()[1]
151 for nick in e.arguments()[2].split():
152 if nick[0] in self.nick_symbols:
153 mode = self.nick_symbols[nick[0]]
154 nick = nick[1:]
155 self.channels[ch].set_mode(mode, nick)
156 self.channels[ch].add_user(nick)
158 def _on_nick(self, c, e):
159 """[Internal]"""
160 before = nm_to_n(e.source())
161 after = e.target()
162 for ch in self.channels.values():
163 if ch.has_user(before):
164 ch.change_nick(before, after)
166 def _on_part(self, c, e):
167 """[Internal]"""
168 nick = nm_to_n(e.source())
169 channel = e.target()
171 if nick == c.get_nickname():
172 del self.channels[channel]
173 else:
174 self.channels[channel].remove_user(nick)
176 def _on_quit(self, c, e):
177 """[Internal]"""
178 nick = nm_to_n(e.source())
179 for ch in self.channels.values():
180 if ch.has_user(nick):
181 ch.remove_user(nick)
183 def die(self, msg="Bye, cruel world!"):
184 """Let the bot die.
186 Arguments:
188 msg -- Quit message.
191 self.connection.disconnect(msg)
192 sys.exit(0)
194 def disconnect(self, msg="I'll be back!"):
195 """Disconnect the bot.
197 The bot will try to reconnect after a while.
199 Arguments:
201 msg -- Quit message.
203 self.connection.disconnect(msg)
205 def get_version(self):
206 """Returns the bot version.
208 Used when answering a CTCP VERSION request.
210 return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
212 def jump_server(self, msg="Changing servers"):
213 """Connect to a new server, possibly disconnecting from the current.
215 The bot will skip to next server in the server_list each time
216 jump_server is called.
218 if self.connection.is_connected():
219 self.connection.disconnect(msg)
221 self.server_list.append(self.server_list.pop(0))
222 self._connect()
224 def on_ctcp(self, c, e):
225 """Default handler for ctcp events.
227 Replies to VERSION and PING requests and relays DCC requests
228 to the on_dccchat method.
230 if e.arguments()[0] == "VERSION":
231 c.ctcp_reply(nm_to_n(e.source()),
232 "VERSION " + self.get_version())
233 elif e.arguments()[0] == "PING":
234 if len(e.arguments()) > 1:
235 c.ctcp_reply(nm_to_n(e.source()),
236 "PING " + e.arguments()[1])
237 elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT":
238 self.on_dccchat(c, e)
240 def on_dccchat(self, c, e):
241 pass
243 def start(self):
244 """Start the bot."""
245 self._connect()
246 SimpleIRCClient.start(self)
249 class IRCDict:
250 """A dictionary suitable for storing IRC-related things.
252 Dictionary keys a and b are considered equal if and only if
253 irc_lower(a) == irc_lower(b)
255 Otherwise, it should behave exactly as a normal dictionary.
258 def __init__(self, dict=None):
259 self.data = {}
260 self.canon_keys = {} # Canonical keys
261 if dict is not None:
262 self.update(dict)
263 def __repr__(self):
264 return repr(self.data)
265 def __cmp__(self, dict):
266 if isinstance(dict, IRCDict):
267 return cmp(self.data, dict.data)
268 else:
269 return cmp(self.data, dict)
270 def __len__(self):
271 return len(self.data)
272 def __getitem__(self, key):
273 return self.data[self.canon_keys[irc_lower(key)]]
274 def __setitem__(self, key, item):
275 if key in self:
276 del self[key]
277 self.data[key] = item
278 self.canon_keys[irc_lower(key)] = key
279 def __delitem__(self, key):
280 ck = irc_lower(key)
281 del self.data[self.canon_keys[ck]]
282 del self.canon_keys[ck]
283 def __iter__(self):
284 return iter(self.data)
285 def __contains__(self, key):
286 return self.has_key(key)
287 def clear(self):
288 self.data.clear()
289 self.canon_keys.clear()
290 def copy(self):
291 if self.__class__ is UserDict:
292 return UserDict(self.data)
293 import copy
294 return copy.copy(self)
295 def keys(self):
296 return self.data.keys()
297 def items(self):
298 return self.data.items()
299 def values(self):
300 return self.data.values()
301 def has_key(self, key):
302 return irc_lower(key) in self.canon_keys
303 def update(self, dict):
304 for k, v in dict.items():
305 self.data[k] = v
306 def get(self, key, failobj=None):
307 if key in self:
308 return self[key]
309 else:
310 return failobj
313 class Channel:
314 """A class for keeping information about an IRC channel.
316 This class can be improved a lot.
319 def __init__(self, name):
320 self.name = name
321 self.userdict = IRCDict()
322 self.ownerdict = IRCDict()
323 self.admindict = IRCDict()
324 self.operdict = IRCDict()
325 self.hopdict = IRCDict()
326 self.voiceddict = IRCDict()
327 self.modes = {}
328 self.modedicts = {'q':self.ownerdict,
329 'a':self.admindict,
330 'o':self.operdict,
331 'h':self.hopdict,
332 'v':self.voiceddict}
334 def users(self):
335 """Returns an unsorted list of the channel's users."""
336 return self.userdict.keys()
338 def opers(self):
339 """Returns an unsorted list of the channel's operators."""
340 return self.operdict.keys()
342 def voiced(self):
343 """Returns an unsorted list of the persons that have voice
344 mode set in the channel."""
345 return self.voiceddict.keys()
347 def has_user(self, nick):
348 """Check whether the channel has a user."""
349 return nick in self.userdict
351 def is_oper(self, nick):
352 """Check whether a user has operator status in the channel."""
353 return nick in self.operdict
355 def is_voiced(self, nick):
356 """Check whether a user has voice mode set in the channel."""
357 return nick in self.voiceddict
359 def add_user(self, nick):
360 self.userdict[nick] = 1
362 def remove_user(self, nick):
363 if nick in self.userdict:
364 del self.userdict[nick]
365 for d in self.modedicts.values():
366 if nick in d:
367 del d[nick]
369 def change_nick(self, before, after):
370 self.userdict[after] = 1
371 del self.userdict[before]
372 for d in self.modedicts.values():
373 if before in d:
374 d[after] = 1
375 del d[before]
377 def set_mode(self, mode, value=None, connection=None):
378 """Set mode on the channel.
380 Arguments:
382 mode -- The mode (a single-character string).
384 value -- Value
386 if mode in self.modedicts:
387 self.modedicts[mode][value] = 1
388 else:
389 self.modes[mode] = value
391 def clear_mode(self, mode, value=None, connection=None):
392 """Clear mode on the channel.
394 Arguments:
396 mode -- The mode (a single-character string).
398 value -- Value
400 try:
401 if mode in self.modedicts:
402 if connection:
403 connection.names([self.name])
404 del self.modedicts[mode][value]
405 else:
406 del self.modes[mode]
407 except KeyError:
408 pass
410 def has_mode(self, mode):
411 return mode in self.modes
413 def is_moderated(self):
414 return self.has_mode("m")
416 def is_secret(self):
417 return self.has_mode("s")
419 def is_protected(self):
420 return self.has_mode("p")
422 def has_topic_lock(self):
423 return self.has_mode("t")
425 def is_invite_only(self):
426 return self.has_mode("i")
428 def has_allow_external_messages(self):
429 return self.has_mode("n")
431 def has_limit(self):
432 return self.has_mode("l")
434 def limit(self):
435 if self.has_limit():
436 return self.modes[l]
437 else:
438 return None
440 def has_key(self):
441 return self.has_mode("k")
443 def key(self):
444 if self.has_key():
445 return self.modes["k"]
446 else:
447 return None