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
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.
45 def __init__(self
, server_list
, nickname
, realname
, reconnection_interval
=60):
46 """Constructor for SingleServerIRCBot objects.
50 server_list -- A list of tuples (server, port) that
51 defines which servers the bot should try 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
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
),
79 def _connected_checker(self
):
81 if not self
.connection
.is_connected():
82 self
.connection
.execute_delayed(self
.reconnection_interval
,
83 self
._connected
_checker
)
89 if len(self
.server_list
[0]) > 2:
90 password
= self
.server_list
[0][2]
92 self
.connect(self
.server_list
[0][0],
93 self
.server_list
[0][1],
96 ircname
=self
._realname
)
97 except ServerConnectionError
:
100 def _on_disconnect(self
, c
, e
):
102 self
.channels
= IRCDict()
103 self
.connection
.execute_delayed(self
.reconnection_interval
,
104 self
._connected
_checker
)
106 def _on_join(self
, c
, e
):
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
):
116 nick
= e
.arguments()[0]
119 if nick
== c
.get_nickname():
120 del self
.channels
[channel
]
122 self
.channels
[channel
].remove_user(nick
)
124 def _on_mode(self
, c
, e
):
126 modes
= parse_channel_modes(" ".join(e
.arguments()))
129 ch
= self
.channels
[t
]
135 f(mode
[1], mode
[2], self
.connection
)
137 # Mode on self... XXX
140 def _on_namreply(self
, c
, e
):
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():
153 self
.channels
[ch
].set_mode("q", nick
)
156 self
.channels
[ch
].set_mode("a", nick
)
159 self
.channels
[ch
].set_mode("o", nick
)
162 self
.channels
[ch
].set_mode("h", nick
)
165 self
.channels
[ch
].set_mode("v", nick
)
166 self
.channels
[ch
].add_user(nick
)
168 def _on_nick(self
, c
, e
):
170 before
= nm_to_n(e
.source())
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
):
178 nick
= nm_to_n(e
.source())
181 if nick
== c
.get_nickname():
182 del self
.channels
[channel
]
184 self
.channels
[channel
].remove_user(nick
)
186 def _on_quit(self
, c
, e
):
188 nick
= nm_to_n(e
.source())
189 for ch
in self
.channels
.values():
190 if ch
.has_user(nick
):
193 def die(self
, msg
="Bye, cruel world!"):
201 self
.connection
.disconnect(msg
)
204 def disconnect(self
, msg
="I'll be back!"):
205 """Disconnect the bot.
207 The bot will try to reconnect after a while.
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))
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
):
256 SimpleIRCClient
.start(self
)
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):
270 self
.canon_keys
= {} # Canonical keys
274 return repr(self
.data
)
275 def __cmp__(self
, dict):
276 if isinstance(dict, IRCDict
):
277 return cmp(self
.data
, dict.data
)
279 return cmp(self
.data
, dict)
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
):
287 self
.data
[key
] = item
288 self
.canon_keys
[irc_lower(key
)] = key
289 def __delitem__(self
, key
):
291 del self
.data
[self
.canon_keys
[ck
]]
292 del self
.canon_keys
[ck
]
294 return iter(self
.data
)
295 def __contains__(self
, key
):
296 return self
.has_key(key
)
299 self
.canon_keys
.clear()
301 if self
.__class
__ is UserDict
:
302 return UserDict(self
.data
)
304 return copy
.copy(self
)
306 return self
.data
.keys()
308 return self
.data
.items()
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():
316 def get(self
, key
, failobj
=None):
324 """A class for keeping information about an IRC channel.
326 This class can be improved a lot.
329 def __init__(self
, name
):
331 self
.userdict
= IRCDict()
332 self
.ownerdict
= IRCDict()
333 self
.admindict
= IRCDict()
334 self
.operdict
= IRCDict()
335 self
.hopdict
= IRCDict()
336 self
.voiceddict
= IRCDict()
338 self
.modedicts
= {'q':self
.ownerdict
,
345 """Returns an unsorted list of the channel's users."""
346 return self
.userdict
.keys()
349 """Returns an unsorted list of the channel's operators."""
350 return self
.operdict
.keys()
353 """Returns an unsorted list of the persons that have voice
354 mode set in the channel."""
355 return self
.voiceddict
.keys()
357 def has_user(self
, nick
):
358 """Check whether the channel has a user."""
359 return nick
in self
.userdict
361 def is_oper(self
, nick
):
362 """Check whether a user has operator status in the channel."""
363 return nick
in self
.operdict
365 def is_voiced(self
, nick
):
366 """Check whether a user has voice mode set in the channel."""
367 return nick
in self
.voiceddict
369 def add_user(self
, nick
):
370 self
.userdict
[nick
] = 1
372 def remove_user(self
, nick
):
373 if nick
in self
.userdict
:
374 del self
.userdict
[nick
]
375 for d
in self
.modedicts
:
379 def change_nick(self
, before
, after
):
380 self
.userdict
[after
] = 1
381 del self
.userdict
[before
]
382 for d
in self
.modedicts
:
387 def set_mode(self
, mode
, value
=None, connection
=None):
388 """Set mode on the channel.
392 mode -- The mode (a single-character string).
396 if mode
in self
.modedicts
:
397 self
.modedicts
[mode
][value
] = 1
399 self
.modes
[mode
] = value
401 def clear_mode(self
, mode
, value
=None, connection
=None):
402 """Clear mode on the channel.
406 mode -- The mode (a single-character string).
411 if mode
in self
.modedicts
:
413 connection
.names([self
.name
])
414 del self
.modedicts
[mode
][value
]
420 def has_mode(self
, mode
):
421 return mode
in self
.modes
423 def is_moderated(self
):
424 return self
.has_mode("m")
427 return self
.has_mode("s")
429 def is_protected(self
):
430 return self
.has_mode("p")
432 def has_topic_lock(self
):
433 return self
.has_mode("t")
435 def is_invite_only(self
):
436 return self
.has_mode("i")
438 def has_allow_external_messages(self
):
439 return self
.has_mode("n")
442 return self
.has_mode("l")
451 return self
.has_mode("k")
455 return self
.modes
["k"]