chiark / gitweb /
correct typo in trout from Emperor
[irc.git] / ircbot.py
1 # Copyright (C) 1999, 2000 Joel Rosdahl\r
2\r
3 # This program is free software; you can redistribute it and/or\r
4 # modify it under the terms of the GNU General Public License\r
5 # as published by the Free Software Foundation; either version 2\r
6 # of the License, or (at your option) any later version.\r
7\r
8 # This program is distributed in the hope that it will be useful,\r
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of\r
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
11 # GNU General Public License for more details.\r
12\r
13 # You should have received a copy of the GNU General Public License\r
14 # along with this program; if not, write to the Free Software\r
15 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\r
16 #\r
17 # Joel Rosdahl <joel@rosdahl.net>\r
18 #\r
19 # $Id: ircbot.py,v 1.1 2002/02/07 09:15:32 matthew Exp $\r
20 \r
21 """ircbot -- Simple IRC bot library.\r
22 \r
23 This module contains a single-server IRC bot class that can be used to\r
24 write simpler bots.\r
25 """\r
26 \r
27 import sys\r
28 import string\r
29 from UserDict import UserDict\r
30 \r
31 from irclib import SimpleIRCClient\r
32 from irclib import nm_to_n, irc_lower, all_events\r
33 from irclib import parse_channel_modes, is_channel, is_channel\r
34 from irclib import ServerConnectionError\r
35 \r
36 class SingleServerIRCBot(SimpleIRCClient):\r
37     """A single-server IRC bot class.\r
38 \r
39     The bot tries to reconnect if it is disconnected.\r
40 \r
41     The bot keeps track of the channels it has joined, the other\r
42     clients that are present in the channels and which of those that\r
43     have operator or voice modes.  The "database" is kept in the\r
44     self.channels attribute, which is an IRCDict of Channels.\r
45     """\r
46     def __init__(self, server_list, nickname, realname, reconnection_interval=60):\r
47         """Constructor for SingleServerIRCBot objects.\r
48 \r
49         Arguments:\r
50 \r
51             server_list -- A list of tuples (server, port) that\r
52                            defines which servers the bot should try to\r
53                            connect to.\r
54 \r
55             nickname -- The bot's nickname.\r
56 \r
57             realname -- The bot's realname.\r
58 \r
59             reconnection_interval -- How long the bot should wait\r
60                                      before trying to reconnect.\r
61         """\r
62 \r
63         SimpleIRCClient.__init__(self)\r
64         self.channels = IRCDict()\r
65         self.server_list = server_list\r
66         if not reconnection_interval or reconnection_interval < 0:\r
67             reconnection_interval = 2**31\r
68         self.reconnection_interval = reconnection_interval\r
69 \r
70         self._nickname = nickname\r
71         self._realname = realname\r
72         for i in ["disconnect", "join", "kick", "mode",\r
73                   "namreply", "nick", "part", "quit"]:\r
74             self.connection.add_global_handler(i,\r
75                                                getattr(self, "_on_" + i),\r
76                                                -10)\r
77     def _connected_checker(self):\r
78         """[Internal]"""\r
79         if not self.connection.is_connected():\r
80             self.connection.execute_delayed(self.reconnection_interval,\r
81                                             self._connected_checker)\r
82             self.jump_server()\r
83 \r
84     def _connect(self):\r
85         """[Internal]"""\r
86         password = None\r
87         if len(self.server_list[0]) > 2:\r
88             password = self.server_list[0][2]\r
89         try:\r
90             self.connect(self.server_list[0][0],\r
91                          self.server_list[0][1],\r
92                          self._nickname,\r
93                          password,\r
94                          ircname=self._realname)\r
95         except ServerConnectionError:\r
96             pass\r
97 \r
98     def _on_disconnect(self, c, e):\r
99         """[Internal]"""\r
100         self.channels = IRCDict()\r
101         self.connection.execute_delayed(self.reconnection_interval,\r
102                                         self._connected_checker)\r
103 \r
104     def _on_join(self, c, e):\r
105         """[Internal]"""\r
106         ch = e.target()\r
107         nick = nm_to_n(e.source())\r
108         if nick == self._nickname:\r
109             self.channels[ch] = Channel()\r
110         self.channels[ch].add_user(nick)\r
111 \r
112     def _on_kick(self, c, e):\r
113         """[Internal]"""\r
114         nick = e.arguments()[0]\r
115         channel = e.target()\r
116 \r
117         if nick == self._nickname:\r
118             del self.channels[channel]\r
119         else:\r
120             self.channels[channel].remove_user(nick)\r
121 \r
122     def _on_mode(self, c, e):\r
123         """[Internal]"""\r
124         modes = parse_channel_modes(string.join(e.arguments()))\r
125         t = e.target()\r
126         if is_channel(t):\r
127             ch = self.channels[t]\r
128             for mode in modes:\r
129                 if mode[0] == "+":\r
130                     f = ch.set_mode\r
131                 else:\r
132                     f = ch.clear_mode\r
133                 f(mode[1], mode[2])\r
134         else:\r
135             # Mode on self... XXX\r
136             pass\r
137 \r
138     def _on_namreply(self, c, e):\r
139         """[Internal]"""\r
140 \r
141         # e.arguments()[0] == "="     (why?)\r
142         # e.arguments()[1] == channel\r
143         # e.arguments()[2] == nick list\r
144 \r
145         ch = e.arguments()[1]\r
146         for nick in string.split(e.arguments()[2]):\r
147             if nick[0] == "@":\r
148                 nick = nick[1:]\r
149                 self.channels[ch].set_mode("o", nick)\r
150             elif nick[0] == "+":\r
151                 nick = nick[1:]\r
152                 self.channels[ch].set_mode("v", nick)\r
153             self.channels[ch].add_user(nick)\r
154 \r
155     def _on_nick(self, c, e):\r
156         """[Internal]"""\r
157         before = nm_to_n(e.source())\r
158         after = e.target()\r
159         for ch in self.channels.values():\r
160             if ch.has_user(before):\r
161                 ch.change_nick(before, after)\r
162         if nm_to_n(before) == self._nickname:\r
163             self._nickname = after\r
164 \r
165     def _on_part(self, c, e):\r
166         """[Internal]"""\r
167         nick = nm_to_n(e.source())\r
168         channel = e.target()\r
169 \r
170         if nick == self._nickname:\r
171             del self.channels[channel]\r
172         else:\r
173             self.channels[channel].remove_user(nick)\r
174 \r
175     def _on_quit(self, c, e):\r
176         """[Internal]"""\r
177         nick = nm_to_n(e.source())\r
178         for ch in self.channels.values():\r
179             if ch.has_user(nick):\r
180                 ch.remove_user(nick)\r
181 \r
182     def die(self, msg="Bye, cruel world!"):\r
183         """Let the bot die.\r
184 \r
185         Arguments:\r
186 \r
187             msg -- Quit message.\r
188         """\r
189         self.connection.quit(msg)\r
190         sys.exit(0)\r
191 \r
192     def disconnect(self, msg="I'll be back!"):\r
193         """Disconnect the bot.\r
194 \r
195         The bot will try to reconnect after a while.\r
196 \r
197         Arguments:\r
198 \r
199             msg -- Quit message.\r
200         """\r
201         self.connection.quit(msg)\r
202 \r
203     def get_version(self):\r
204         """Returns the bot version.\r
205 \r
206         Used when answering a CTCP VERSION request.\r
207         """\r
208         return "VERSION ircbot.py by Joel Rosdahl <joel@rosdahl.net>, Matthew Vernon <matthew@debian.org> and others"\r
209 \r
210     def jump_server(self):\r
211         """Connect to a new server, possible disconnecting from the current.\r
212 \r
213         The bot will skip to next server in the server_list each time\r
214         jump_server is called.\r
215         """\r
216         if self.connection.is_connected():\r
217             self.connection.quit("Jumping servers")\r
218         self.server_list.append(self.server_list.pop(0))\r
219         self._connect()\r
220 \r
221     def on_ctcp(self, c, e):\r
222         """Default handler for ctcp events.\r
223 \r
224         Replies to VERSION and PING requests.\r
225         """\r
226         if e.arguments()[0] == "VERSION":\r
227             c.ctcp_reply(nm_to_n(e.source()), self.get_version())\r
228         elif e.arguments()[0] == "PING":\r
229             if len(e.arguments()) > 1:\r
230                 c.ctcp_reply(nm_to_n(e.source()),\r
231                              "PING " + e.arguments()[1])\r
232 \r
233     def start(self):\r
234         """Start the bot."""\r
235         self._connect()\r
236         SimpleIRCClient.start(self)\r
237 \r
238 \r
239 class IRCDict:\r
240     """A dictionary suitable for storing IRC-related things.\r
241 \r
242     Dictionary keys a and b are considered equal if and only if\r
243     irc_lower(a) == irc_lower(b)\r
244 \r
245     Otherwise, it should behave exactly as a normal dictionary.\r
246     """\r
247 \r
248     def __init__(self, dict=None):\r
249         self.data = {}\r
250         self.canon_keys = {}  # Canonical keys\r
251         if dict is not None:\r
252             self.update(dict)\r
253     def __repr__(self):\r
254         return repr(self.data)\r
255     def __cmp__(self, dict):\r
256         if isinstance(dict, IRCDict):\r
257             return cmp(self.data, dict.data)\r
258         else:\r
259             return cmp(self.data, dict)\r
260     def __len__(self):\r
261         return len(self.data)\r
262     def __getitem__(self, key):\r
263         return self.data[self.canon_keys[irc_lower(key)]]\r
264     def __setitem__(self, key, item):\r
265         if self.has_key(key):\r
266             del self[key]\r
267         self.data[key] = item\r
268         self.canon_keys[irc_lower(key)] = key\r
269     def __delitem__(self, key):\r
270         ck = irc_lower(key)\r
271         del self.data[self.canon_keys[ck]]\r
272         del self.canon_keys[ck]\r
273     def clear(self):\r
274         self.data.clear()\r
275         self.canon_keys.clear()\r
276     def copy(self):\r
277         if self.__class__ is UserDict:\r
278             return UserDict(self.data)\r
279         import copy\r
280         return copy.copy(self)\r
281     def keys(self):\r
282         return self.data.keys()\r
283     def items(self):\r
284         return self.data.items()\r
285     def values(self):\r
286         return self.data.values()\r
287     def has_key(self, key):\r
288         return self.canon_keys.has_key(irc_lower(key))\r
289     def update(self, dict):\r
290         for k, v in dict.items():\r
291             self.data[k] = v\r
292     def get(self, key, failobj=None):\r
293         return self.data.get(key, failobj)\r
294 \r
295 \r
296 class Channel:\r
297     """A class for keeping information about an IRC channel.\r
298 \r
299     This class can be improved a lot.\r
300     """\r
301 \r
302     def __init__(self):\r
303         self.userdict = IRCDict()\r
304         self.operdict = IRCDict()\r
305         self.voiceddict = IRCDict()\r
306         self.modes = {}\r
307 \r
308     def users(self):\r
309         """Returns an unsorted list of the channel's users."""\r
310         return self.userdict.keys()\r
311 \r
312     def opers(self):\r
313         """Returns an unsorted list of the channel's operators."""\r
314         return self.operdict.keys()\r
315 \r
316     def voiced(self):\r
317         """Returns an unsorted list of the persons that have voice\r
318         mode set in the channel."""\r
319         return self.voiceddict.keys()\r
320 \r
321     def has_user(self, nick):\r
322         """Check whether the channel has a user."""\r
323         return self.userdict.has_key(nick)\r
324 \r
325     def is_oper(self, nick):\r
326         """Check whether a user has operator status in the channel."""\r
327         return self.operdict.has_key(nick)\r
328 \r
329     def is_voiced(self, nick):\r
330         """Check whether a user has voice mode set in the channel."""\r
331         return self.voiceddict.has_key(nick)\r
332 \r
333     def add_user(self, nick):\r
334         self.userdict[nick] = 1\r
335 \r
336     def remove_user(self, nick):\r
337         for d in self.userdict, self.operdict, self.voiceddict:\r
338             if d.has_key(nick):\r
339                 del d[nick]\r
340 \r
341     def change_nick(self, before, after):\r
342         self.userdict[after] = 1\r
343         del self.userdict[before]\r
344         if self.operdict.has_key(before):\r
345             self.operdict[after] = 1\r
346             del self.operdict[before]\r
347         if self.voiceddict.has_key(before):\r
348             self.voiceddict[after] = 1\r
349             del self.voiceddict[before]\r
350 \r
351     def set_mode(self, mode, value=None):\r
352         """Set mode on the channel.\r
353 \r
354         Arguments:\r
355 \r
356             mode -- The mode (a single-character string).\r
357 \r
358             value -- Value\r
359         """\r
360         if mode == "o":\r
361             self.operdict[value] = 1\r
362         elif mode == "v":\r
363             self.voiceddict[value] = 1\r
364         else:\r
365             self.modes[mode] = value\r
366 \r
367     def clear_mode(self, mode, value=None):\r
368         """Clear mode on the channel.\r
369 \r
370         Arguments:\r
371 \r
372             mode -- The mode (a single-character string).\r
373 \r
374             value -- Value\r
375         """\r
376         try:\r
377             if mode == "o":\r
378                 del self.operdict[value]\r
379             elif mode == "v":\r
380                 del self.voiceddict[value]\r
381             else:\r
382                 del self.modes[mode]\r
383         except KeyError:\r
384             pass\r
385 \r
386     def has_mode(self, mode):\r
387         return mode in self.modes\r
388 \r
389     def is_moderated(self):\r
390         return self.has_mode("m")\r
391 \r
392     def is_secret(self):\r
393         return self.has_mode("s")\r
394 \r
395     def is_protected(self):\r
396         return self.has_mode("p")\r
397 \r
398     def has_topic_lock(self):\r
399         return self.has_mode("t")\r
400 \r
401     def is_invite_only(self):\r
402         return self.has_mode("i")\r
403 \r
404     def has_message_from_outside_protection(self):\r
405         # Eh... What should it be called, really?\r
406         return self.has_mode("n")\r
407 \r
408     def has_limit(self):\r
409         return self.has_mode("l")\r
410 \r
411     def limit(self):\r
412         if self.has_limit():\r
413             return self.modes[l]\r
414         else:\r
415             return None\r
416 \r
417     def has_key(self):\r
418         return self.has_mode("k")\r
419 \r
420     def key(self):\r
421         if self.has_key():\r
422             return self.modes["k"]\r
423         else:\r
424             return None\r