]> gitweb.pimeys.fr Git - python-myirclib.git/blob - ircbot.py
Il est possible que l'exception n'ait pas d'args
[python-myirclib.git] / ircbot.py
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