Home | History | Annotate | Download | only in irc
      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 (at] 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 (at] 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
    439