class Client(object): """The base IRC Client, which wraps a ConnectionManager and provides an interface to low level connection functions. It can be extended to make a full IRC client or an IRC bot.""" def __init__(self, servers, event_plugins=None): self.channels = [] self._servers = servers self.pinged = False if event_plugins is None: event_plugins = {} self.connection_manager = ConnectionManager(PluginEventManager({ Events.CONNECT: EventHook(self._on_connect), Events.RPL_WELCOME: EventHook(self._on_welcome, source=True, target=True, message=True), Events.PING: EventHook(self._on_ping, message=True), Events.MODE: EventHook(self._on_mode, source=True, target=True, args=True), Events.UMODE: EventHook(self._on_umode, source=True, target=True, message=True), Events.JOIN: EventHook(self._on_join, source=True, message=True), Events.PART: EventHook(self._on_part, source=True, target=True, message=True), Events.QUIT: EventHook(self._on_quit, source=True, message=True), Events.RPL_NAMEREPLY: EventHook(self._on_name_reply, source=True, target=True, args=True, message=True), Events.PRIVMSG: EventHook(self._on_privmsg, source=True, target=True, message=True), Events.PUBMSG: EventHook(self._on_pubmsg, source=True, target=True, message=True), Events.RPL_CHANNELMODEIS: EventHook(self._on_channel_mode, source=True, target=True, args=True), Events.ERR_NICKNAMEINUSE: EventHook(self.on_nickname_in_use), Events.ERROR: EventHook(self._on_error, message=True) }, event_plugins)) self.process_lock = Lock() def start(self): """Starts the Client by first connecting to all given servers and then starting the main loop.""" for server in self._servers: self._connect(server) try: self.on_initial_connect() except NotImplementedError: pass while self.connection_manager.running: with self.process_lock: self.connection_manager.process() def __connect(self, server): connection = Connection(server) connected = connection.connect() if connected: self.connection_manager.register(connection) return connected def _try_connect(self, server): while True: if self.__connect(server): return time.sleep(30) def _connect(self, server): """Performs a connection to the server by creating a Connection object, connecting it, and then registering the new Connection with the ConnectionManager.""" server.reset() if not self.__connect(server): Thread(target=self._try_connect, args=(server,)).start() def on_initial_connect(self): """Function performed after all servers have been connected.""" raise NotImplementedError def exit(self, message=u'Bye!'): """Disconnects from every connection in the ConnectionManager with the given QUIT message.""" with self.process_lock: self.connection_manager.exit(message) def get_server_by_name(self, name): for server in self._servers: if server.name == name: return server return None def get_server_channels(self, server): for channel in self.channels: if channel.server == server: yield channel def find_channel(self, server, name): """Searches for a Channel based on the server and the name. Returns None if the Channel is not found.""" search_channel = Channel(server, name) for channel in self.channels: if search_channel == channel: return channel return None def remove_channel(self, server, name): """Tries to remove a Channel based on the server and the name, and fails silently.""" channel = Channel(server, name) try: self.channels.remove(channel) except ValueError: log.debug(u"Channel `%s' not in channels." % name) def find_connection(self, server): """Searches for a Connection based on the server and compares only the host of the server. Returns None if the Connection is not found.""" for connection in self.connection_manager.connections.itervalues(): if connection.server.host == server.host: return connection return None def connect(self, host, port, nick, use_ssl=False): """Used for connecting to a server not given when creating the Client object.""" server = Server(host, port, nick, use_ssl) self._connect(server) def nick(self, connection, new_nick): """Changes the nick in the given connection.""" connection.send('NICK ' + new_nick) def mode(self, connection, target, mode=None): """Performs the IRC MODE command.""" mode_message = 'MODE %s' % target if mode is not None: mode_message += ' %s' % mode connection.send(mode_message) def privmsg(self, connection, target, message): """Sends a PRIVMSG to a target in the given connection.""" connection.send('PRIVMSG %s :%s' % (target, message)) def notice(self, connection, target, message): """Sends a NOTICE to a target in the given connection.""" connection.send('NOTICE %s :%s' % (target, message)) def ctcp_reply(self, connection, target, command, reply): """Sends a CTCP reply to a target in a given connection.""" self.notice(connection, target, '\x01%s %s\x01' % (command, reply)) def join(self, connection, *channels): """Makes the client join a bunch of channels. Example password protected channel argument: '#mathematics love' where 'love' is the password.""" connection.send('JOIN ' + ','.join(channels)) def part(self, connection, *channels): """Makes the client part from a bunch of channels.""" connection.send('PART ' + ','.join(channels)) def kick(self, connection, channel, user, reason=''): """Kicks a user from a channel in a given connection for a given reason.""" connection.send('KICK %s %s :%s' % (channel, user, reason)) def quit(self, connection, message=u''): """Disconnects the given connection with the given message. This is better than just quitting because it also cleans things up with the connection manager.""" with self.process_lock: self.connection_manager.disconnect(connection, message) def on_connect(self, connection): raise NotImplementedError def _on_connect(self, connection): connection.send('NICK ' + connection.server.nick) connection.send('USER %s 0 * :%s' % (connection.server.nick, connection.server.nick)) try: self.on_connect(connection) except NotImplementedError: pass def on_welcome(self, connection, source, target, message): raise NotImplementedError def _on_welcome(self, connection, source, target, message): connection.server.actual_nick = target try: self.on_welcome(connection, source, target, message) except NotImplementedError: pass def on_nickname_in_use(self, connection): connection.server.nick = connection.server.nick + '_' self._on_connect(connection) def _on_join(self, connection, source, message): if source.nick == connection.server.actual_nick: channel = Channel(connection.server, message) self.channels.append(channel) self.mode(connection, channel) else: channel = self.find_channel(connection.server, message) if channel is not None: channel.add_user(User(source.nick, '', '', User.NORMAL)) def _on_name_reply(self, connection, source, target, args, message): channel_name = args[-1] channel = self.find_channel(connection.server, channel_name) if channel is not None: for user in message.split(): channel.add_user(User.parse_user(user)) def _on_part(self, connection, source, target, message): if source.nick == connection.server.actual_nick: self.remove_channel(connection.server, target) else: channel = self.find_channel(connection.server, target) if channel is not None: channel.remove_user(User(source.nick, '', '', User.NORMAL)) def _on_quit(self, connection, source, message): user = User.channel_user(source.nick) for channel in self.get_server_channels(connection.server): channel.remove_user(user) def on_privmsg(self, connection, source, target, message): raise NotImplementedError def _on_privmsg(self, connection, source, target, message): if message[0] == '\x01' and message[-1] == '\x01': self._on_ctcp(connection, source, message[1:-1]) else: try: self.on_privmsg(connection, source, target, message) except NotImplementedError: pass def on_pubmsg(self, connection, source, target, message): raise NotImplementedError def _on_pubmsg(self, connection, source, target, message): try: self.on_pubmsg(connection, source, target, message) except NotImplementedError: pass def _on_channel_mode(self, connection, source, target, args): (name, modes), mode_args = args[:2], args[2:] channel = self.find_channel(connection.server, name) if channel is not None: channel_modes = Mode.parse_modes(modes, mode_args) for mode in channel_modes: if mode.on: channel.add_mode(mode) else: channel.remove_mode(mode) def get_version(self): raise NotImplementedError def _on_ctcp(self, connection, source, message): command, arg = '', '' split_message = message.split(' ', 1) if len(split_message) > 1: command, arg = split_message else: command = split_message[0] reply = '' if command == 'VERSION': try: reply = self.get_version() except NotImplementedError: pass elif command == 'PING' and arg: reply = arg elif command == 'TIME': reply = ':%s' % datetime.now().ctime() if reply: self.ctcp_reply(connection, source.nick, command, reply) def _on_mode(self, connection, source, target, args): channel = self.find_channel(connection.server, target) if channel is not None: modes, mode_args = args[0], args[1:] channel_modes = Mode.parse_modes(modes, mode_args) for mode in channel_modes: user = channel.find_user(mode.param) if user is not None: if mode.on: user.add_mode(mode) else: user.remove_mode(mode) else: if mode.on: channel.add_mode(mode) else: channel.remove_mode(mode) def _on_umode(self, connection, source, target, message): pass def _on_ping(self, connection, message): connection.send('PONG %s :%s' % (connection.server.actual_host, message)) def _on_error(self, connection, message): log.error(message) self._connect(connection.server)