Initial import
[halbot.git] / ircbot.py
blobd91500ea1b0c3946cf02e7371be6c8d57642bea0
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()
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])
136 else:
137 # Mode on self... XXX
138 pass
140 def _on_namreply(self, c, e):
141 """[Internal]"""
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
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("q", nick)
154 elif nick[0] == "&":
155 nick = nick[1:]
156 self.channels[ch].set_mode("a", nick)
157 elif nick[0] == "@":
158 nick = nick[1:]
159 self.channels[ch].set_mode("o", nick)
160 elif nick[0] == "%":
161 nick = nick[1:]
162 self.channels[ch].set_mode("h", nick)
163 elif nick[0] == "+":
164 nick = nick[1:]
165 self.channels[ch].set_mode("v", nick)
166 self.channels[ch].add_user(nick)
168 def _on_nick(self, c, e):
169 """[Internal]"""
170 before = nm_to_n(e.source())
171 after = e.target()
172 for ch in self.channels.values():
173 if ch.has_user(before):
174 ch.change_nick(before, after)
176 def _on_part(self, c, e):
177 """[Internal]"""
178 nick = nm_to_n(e.source())
179 channel = e.target()
181 if nick == c.get_nickname():
182 del self.channels[channel]
183 else:
184 self.channels[channel].remove_user(nick)
186 def _on_quit(self, c, e):
187 """[Internal]"""
188 nick = nm_to_n(e.source())
189 for ch in self.channels.values():
190 if ch.has_user(nick):
191 ch.remove_user(nick)
193 def die(self, msg="Bye, cruel world!"):
194 """Let the bot die.
196 Arguments:
198 msg -- Quit message.
201 self.connection.disconnect(msg)
202 sys.exit(0)
204 def disconnect(self, msg="I'll be back!"):
205 """Disconnect the bot.
207 The bot will try to reconnect after a while.
209 Arguments:
211 msg -- Quit message.
213 self.connection.disconnect(msg)
215 def get_version(self):
216 """Returns the bot version.
218 Used when answering a CTCP VERSION request.
220 return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
222 def jump_server(self, msg="Changing servers"):
223 """Connect to a new server, possibly disconnecting from the current.
225 The bot will skip to next server in the server_list each time
226 jump_server is called.
228 if self.connection.is_connected():
229 self.connection.disconnect(msg)
231 self.server_list.append(self.server_list.pop(0))
232 self._connect()
234 def on_ctcp(self, c, e):
235 """Default handler for ctcp events.
237 Replies to VERSION and PING requests and relays DCC requests
238 to the on_dccchat method.
240 if e.arguments()[0] == "VERSION":
241 c.ctcp_reply(nm_to_n(e.source()),
242 "VERSION " + self.get_version())
243 elif e.arguments()[0] == "PING":
244 if len(e.arguments()) > 1:
245 c.ctcp_reply(nm_to_n(e.source()),
246 "PING " + e.arguments()[1])
247 elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT":
248 self.on_dccchat(c, e)
250 def on_dccchat(self, c, e):
251 pass
253 def start(self):
254 """Start the bot."""
255 self._connect()
256 SimpleIRCClient.start(self)
259 class IRCDict:
260 """A dictionary suitable for storing IRC-related things.
262 Dictionary keys a and b are considered equal if and only if
263 irc_lower(a) == irc_lower(b)
265 Otherwise, it should behave exactly as a normal dictionary.
268 def __init__(self, dict=None):
269 self.data = {}
270 self.canon_keys = {} # Canonical keys
271 if dict is not None:
272 self.update(dict)
273 def __repr__(self):
274 return repr(self.data)
275 def __cmp__(self, dict):
276 if isinstance(dict, IRCDict):
277 return cmp(self.data, dict.data)
278 else:
279 return cmp(self.data, dict)
280 def __len__(self):
281 return len(self.data)
282 def __getitem__(self, key):
283 return self.data[self.canon_keys[irc_lower(key)]]
284 def __setitem__(self, key, item):
285 if key in self:
286 del self[key]
287 self.data[key] = item
288 self.canon_keys[irc_lower(key)] = key
289 def __delitem__(self, key):
290 ck = irc_lower(key)
291 del self.data[self.canon_keys[ck]]
292 del self.canon_keys[ck]
293 def __iter__(self):
294 return iter(self.data)
295 def __contains__(self, key):
296 return self.has_key(key)
297 def clear(self):
298 self.data.clear()
299 self.canon_keys.clear()
300 def copy(self):
301 if self.__class__ is UserDict:
302 return UserDict(self.data)
303 import copy
304 return copy.copy(self)
305 def keys(self):
306 return self.data.keys()
307 def items(self):
308 return self.data.items()
309 def values(self):
310 return self.data.values()
311 def has_key(self, key):
312 return irc_lower(key) in self.canon_keys
313 def update(self, dict):
314 for k, v in dict.items():
315 self.data[k] = v
316 def get(self, key, failobj=None):
317 return self.data.get(key, failobj)
320 class Channel:
321 """A class for keeping information about an IRC channel.
323 This class can be improved a lot.
326 def __init__(self):
327 self.userdict = IRCDict()
328 self.ownerdict = IRCDict()
329 self.admindict = IRCDict()
330 self.operdict = IRCDict()
331 self.hopdict = IRCDict()
332 self.voiceddict = IRCDict()
333 self.modes = {}
335 def users(self):
336 """Returns an unsorted list of the channel's users."""
337 return self.userdict.keys()
339 def opers(self):
340 """Returns an unsorted list of the channel's operators."""
341 return self.operdict.keys()
343 def voiced(self):
344 """Returns an unsorted list of the persons that have voice
345 mode set in the channel."""
346 return self.voiceddict.keys()
348 def has_user(self, nick):
349 """Check whether the channel has a user."""
350 return nick in self.userdict
352 def is_oper(self, nick):
353 """Check whether a user has operator status in the channel."""
354 return nick in self.operdict
356 def is_voiced(self, nick):
357 """Check whether a user has voice mode set in the channel."""
358 return nick in self.voiceddict
360 def add_user(self, nick):
361 self.userdict[nick] = 1
363 def remove_user(self, nick):
364 for d in self.userdict, self.operdict, self.voiceddict:
365 if nick in d:
366 del d[nick]
368 def change_nick(self, before, after):
369 self.userdict[after] = 1
370 del self.userdict[before]
371 if before in self.operdict:
372 self.operdict[after] = 1
373 del self.operdict[before]
374 if before in self.voiceddict:
375 self.voiceddict[after] = 1
376 del self.voiceddict[before]
378 def set_mode(self, mode, value=None):
379 """Set mode on the channel.
381 Arguments:
383 mode -- The mode (a single-character string).
385 value -- Value
387 if mode == "q":
388 self.ownerdict[value] = 1
389 elif mode == "a":
390 self.admindict[value] = 1
391 elif mode == "o":
392 self.operdict[value] = 1
393 elif mode == "h":
394 self.hopdict[value] = 1
395 elif mode == "v":
396 self.voiceddict[value] = 1
397 else:
398 self.modes[mode] = value
400 def clear_mode(self, mode, value=None):
401 """Clear mode on the channel.
403 Arguments:
405 mode -- The mode (a single-character string).
407 value -- Value
409 try:
410 if mode == "q":
411 del self.operdict[value]
412 elif mode == "a":
413 del self.admindict[value]
414 elif mode == "o":
415 del self.operdict[value]
416 elif mode == "h":
417 del self.hopdict[value]
418 elif mode == "v":
419 del self.voiceddict[value]
420 else:
421 del self.modes[mode]
422 except KeyError:
423 pass
425 def has_mode(self, mode):
426 return mode in self.modes
428 def is_moderated(self):
429 return self.has_mode("m")
431 def is_secret(self):
432 return self.has_mode("s")
434 def is_protected(self):
435 return self.has_mode("p")
437 def has_topic_lock(self):
438 return self.has_mode("t")
440 def is_invite_only(self):
441 return self.has_mode("i")
443 def has_allow_external_messages(self):
444 return self.has_mode("n")
446 def has_limit(self):
447 return self.has_mode("l")
449 def limit(self):
450 if self.has_limit():
451 return self.modes[l]
452 else:
453 return None
455 def has_key(self):
456 return self.has_mode("k")
458 def key(self):
459 if self.has_key():
460 return self.modes["k"]
461 else:
462 return None