Skip to content
Newer
Older
100644 444 lines (360 sloc) 13.1 KB
36aca6b @puzzlet py3 branch based on python-irclib 0.4.8
authored
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
29 from irclib import SimpleIRCClient
30 from irclib import nm_to_n, irc_lower
31 from irclib import parse_channel_modes, is_channel
32 from irclib import ServerConnectionError
33
34 class SingleServerIRCBot(SimpleIRCClient):
35 """A single-server IRC bot class.
36
37 The bot tries to reconnect if it is disconnected.
38
39 The bot keeps track of the channels it has joined, the other
40 clients that are present in the channels and which of those that
41 have operator or voice modes. The "database" is kept in the
42 self.channels attribute, which is an IRCDict of Channels.
43 """
44 def __init__(self, server_list, nickname, username=None, realname=None,
45 reconnection_interval=60, use_ssl=False):
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 use_ssl -- Whether to use SSL in connection
65 """
66
67 SimpleIRCClient.__init__(self)
68 self.channels = IRCDict()
69 self.server_list = server_list
70 if not reconnection_interval or reconnection_interval < 0:
71 reconnection_interval = 2**31
72 self.reconnection_interval = reconnection_interval
73 self.use_ssl = use_ssl
74
75 self._nickname = nickname
76 self._username = username or nickname
77 self._realname = realname or nickname
78 for i in ["disconnect", "join", "kick", "mode",
79 "namreply", "nick", "part", "quit"]:
80 self.connection.add_global_handler(i,
81 getattr(self, "_on_" + i),
82 -10)
83 def _connected_checker(self):
84 """[Internal]"""
85 if not self.connection.is_connected():
86 self.connection.execute_delayed(self.reconnection_interval,
87 self._connected_checker)
88 self.jump_server()
89
90 def _connect(self):
91 """[Internal]"""
92 password = None
93 if len(self.server_list[0]) > 2:
94 password = self.server_list[0][2]
95 try:
96 self.connect(self.server_list[0][0],
97 self.server_list[0][1],
98 self._nickname,
99 password,
100 username=self._username,
101 ircname=self._realname,
102 ssl=self.use_ssl)
103 except ServerConnectionError:
104 pass
105
106 def _on_disconnect(self, c, e):
107 """[Internal]"""
108 self.channels = IRCDict()
109 self.connection.execute_delayed(self.reconnection_interval,
110 self._connected_checker)
111
112 def _on_join(self, c, e):
113 """[Internal]"""
114 ch = e.target()
115 nick = nm_to_n(e.source())
116 if nick == c.get_nickname():
117 self.channels[ch] = Channel()
118 self.channels[ch].add_user(nick)
119
120 def _on_kick(self, c, e):
121 """[Internal]"""
122 nick = e.arguments()[0]
123 channel = e.target()
124
125 if nick == c.get_nickname():
126 del self.channels[channel]
127 else:
128 self.channels[channel].remove_user(nick)
129
130 def _on_mode(self, c, e):
131 """[Internal]"""
132 modes = parse_channel_modes(b" ".join(e.arguments()))
133 t = e.target()
134 if is_channel(t):
135 ch = self.channels[t]
136 for mode in modes:
137 if mode[0] == b"+":
138 f = ch.set_mode
139 else:
140 f = ch.clear_mode
141 f(mode[1], mode[2])
142 else:
143 # Mode on self... XXX
144 pass
145
146 def _on_namreply(self, c, e):
147 """[Internal]"""
148
149 # e.arguments()[0] == "@" for secret channels,
150 # "*" for private channels,
151 # "=" for others (public channels)
152 # e.arguments()[1] == channel
153 # e.arguments()[2] == nick list
154
155 ch = e.arguments()[1]
156 for nick in e.arguments()[2].split():
157 if nick[0:1] == b"@":
158 nick = nick[1:]
159 self.channels[ch].set_mode(b"o", nick)
160 elif nick[0:1] == b"+":
161 nick = nick[1:]
162 self.channels[ch].set_mode(b"v", nick)
163 self.channels[ch].add_user(nick)
164
165 def _on_nick(self, c, e):
166 """[Internal]"""
167 before = nm_to_n(e.source())
168 after = e.target()
169 for ch in self.channels.values():
170 if ch.has_user(before):
171 ch.change_nick(before, after)
172
173 def _on_part(self, c, e):
174 """[Internal]"""
175 nick = nm_to_n(e.source())
176 channel = e.target()
177
178 if nick == c.get_nickname():
179 del self.channels[channel]
180 else:
181 self.channels[channel].remove_user(nick)
182
183 def _on_quit(self, c, e):
184 """[Internal]"""
185 nick = nm_to_n(e.source())
186 for ch in self.channels.values():
187 if ch.has_user(nick):
188 ch.remove_user(nick)
189
190 def die(self, msg=b"Bye, cruel world!"):
191 """Let the bot die.
192
193 Arguments:
194
195 msg -- Quit message.
196 """
197
198 self.connection.disconnect(msg)
199 sys.exit(0)
200
201 def disconnect(self, msg=b"I'll be back!"):
202 """Disconnect the bot.
203
204 The bot will try to reconnect after a while.
205
206 Arguments:
207
208 msg -- Quit message.
209 """
210 self.connection.disconnect(msg)
211
212 def get_version(self):
213 """Returns the bot version.
214
215 Used when answering a CTCP VERSION request.
216 """
217 return b"ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
218
219 def jump_server(self, msg="Changing servers"):
220 """Connect to a new server, possibly disconnecting from the current.
221
222 The bot will skip to next server in the server_list each time
223 jump_server is called.
224 """
225 if self.connection.is_connected():
226 self.connection.disconnect(msg)
227
228 self.server_list.append(self.server_list.pop(0))
229 self._connect()
230
231 def on_ctcp(self, c, e):
232 """Default handler for ctcp events.
233
234 Replies to VERSION and PING requests and relays DCC requests
235 to the on_dccchat method.
236 """
237 if e.arguments()[0] == b"VERSION":
238 c.ctcp_reply(nm_to_n(e.source()),
239 b"VERSION " + self.get_version())
240 elif e.arguments()[0] == b"PING":
241 if len(e.arguments()) > 1:
242 c.ctcp_reply(nm_to_n(e.source()),
243 b"PING " + e.arguments()[1])
244 elif e.arguments()[0] == b"DCC" and \
245 e.arguments()[1].split(b" ", 1)[0] == b"CHAT":
246 self.on_dccchat(c, e)
247
248 def on_dccchat(self, c, e):
249 pass
250
251 def start(self):
252 """Start the bot."""
253 self._connect()
254 SimpleIRCClient.start(self)
255
256
257 class IRCDict:
258 """A dictionary suitable for storing IRC-related things.
259
260 Dictionary keys a and b are considered equal if and only if
261 irc_lower(a) == irc_lower(b)
262
263 Otherwise, it should behave exactly as a normal dictionary.
264 """
265
266 def __init__(self, ircdict=None):
267 self.data = {}
268 self.canon_keys = {} # Canonical keys
269 if ircdict is not None:
270 self.update(ircdict)
271 def __repr__(self):
272 return repr(self.data)
273 def __lt__(self, rhs):
274 if isinstance(rhs, IRCDict):
275 return self.data < rhs.data
276 else:
277 return self.data < rhs
278 def __len__(self):
279 return len(self.data)
280 def __getitem__(self, key):
281 return self.data[self.canon_keys[irc_lower(key)]]
282 def __setitem__(self, key, item):
283 if key in self:
284 del self[key]
285 self.data[key] = item
286 self.canon_keys[irc_lower(key)] = key
287 def __delitem__(self, key):
288 ck = irc_lower(key)
289 del self.data[self.canon_keys[ck]]
290 del self.canon_keys[ck]
291 def __iter__(self):
292 return iter(self.data)
293 def __contains__(self, key):
294 return key in self.data
295 def clear(self):
296 self.data.clear()
297 self.canon_keys.clear()
298 def copy(self):
299 import copy
300 return copy.copy(self)
301 def keys(self):
302 return self.data.keys()
303 def items(self):
304 return self.data.items()
305 def values(self):
306 return self.data.values()
307 def has_key(self, key):
308 return irc_lower(key) in self.canon_keys
309 def update(self, ircdict):
310 for k, v in ircdict.items():
311 self.data[k] = v
312 def get(self, key, failobj=None):
313 return self.data.get(key, failobj)
314
315
316 class Channel:
317 """A class for keeping information about an IRC channel.
318
319 This class can be improved a lot.
320 """
321
322 def __init__(self):
323 self.userdict = IRCDict()
324 self.operdict = IRCDict()
325 self.voiceddict = IRCDict()
326 self.modes = {}
327
328 def users(self):
329 """Returns a dictview representing the channel's users."""
330 return self.userdict.keys()
331
332 def opers(self):
333 """Returns a dictview representing the channel's operators."""
334 return self.operdict.keys()
335
336 def voiced(self):
337 """Returns a dictview representing the persons that have voice
338 mode set in the channel."""
339 return self.voiceddict.keys()
340
341 def has_user(self, nick):
342 """Check whether the channel has a user."""
343 return nick in self.userdict
344
345 def is_oper(self, nick):
346 """Check whether a user has operator status in the channel."""
347 return nick in self.operdict
348
349 def is_voiced(self, nick):
350 """Check whether a user has voice mode set in the channel."""
351 return nick in self.voiceddict
352
353 def add_user(self, nick):
354 self.userdict[nick] = 1
355
356 def remove_user(self, nick):
357 for d in self.userdict, self.operdict, self.voiceddict:
358 if nick in d:
359 del d[nick]
360
361 def change_nick(self, before, after):
362 self.userdict[after] = 1
363 del self.userdict[before]
364 if before in self.operdict:
365 self.operdict[after] = 1
366 del self.operdict[before]
367 if before in self.voiceddict:
368 self.voiceddict[after] = 1
369 del self.voiceddict[before]
370
371 def set_mode(self, mode, value=None):
372 """Set mode on the channel.
373
374 Arguments:
375
376 mode -- The mode (a single-character byte).
377
378 value -- Value
379 """
380 if mode == b"o":
381 self.operdict[value] = 1
382 elif mode == b"v":
383 self.voiceddict[value] = 1
384 else:
385 self.modes[mode] = value
386
387 def clear_mode(self, mode, value=None):
388 """Clear mode on the channel.
389
390 Arguments:
391
392 mode -- The mode (a single-character byte).
393
394 value -- Value
395 """
396 try:
397 if mode == b"o":
398 del self.operdict[value]
399 elif mode == b"v":
400 del self.voiceddict[value]
401 else:
402 del self.modes[mode]
403 except KeyError:
404 pass
405
406 def has_mode(self, mode):
407 return mode in self.modes
408
409 def is_moderated(self):
410 return self.has_mode(b"m")
411
412 def is_secret(self):
413 return self.has_mode(b"s")
414
415 def is_protected(self):
416 return self.has_mode(b"p")
417
418 def has_topic_lock(self):
419 return self.has_mode(b"t")
420
421 def is_invite_only(self):
422 return self.has_mode(b"i")
423
424 def has_allow_external_messages(self):
425 return self.has_mode(b"n")
426
427 def has_limit(self):
428 return self.has_mode(b"l")
429
430 def limit(self):
431 if self.has_limit():
432 return self.modes[b"l"]
433 else:
434 return None
435
436 def has_key(self):
437 return self.has_mode(b"k")
438
439 def key(self):
440 if self.has_key():
441 return self.modes[b"k"]
442 else:
443 return None
Something went wrong with that request. Please try again.