Ejemplo n.º 1
0
    def __init__(self, jid, password, room, nick, mq):
        self.nick = nick
        self.room = room
        self.jid = JID(jid)

        bot = ClientXMPP(jid, password)

        # disable ipv6 for now since we're getting errors using it
        bot.use_ipv6 = False

        bot.add_event_handler('session_start', self.on_start)
        bot.add_event_handler('message', self.on_message)

        bot.register_plugin('xep_0045')
        self._muc = bot.plugin['xep_0045']
        bot.register_plugin('xep_0199')
        bot.plugin['xep_0199'].enable_keepalive(30, 30)
        resource = 'sweetiewatch' + self.randomstr()

        self.mq = mq

        if not bot.connect(): raise 'could not connect'
        bot.process()

        self._bot = bot
Ejemplo n.º 2
0
    def __init__(self, jid, password, room, nick, mq):
        self.nick = nick
        self.room = room
        self.jid = JID(jid)

        bot = ClientXMPP(jid, password)

        # disable ipv6 for now since we're getting errors using it
        bot.use_ipv6 = False

        bot.add_event_handler('session_start', self.on_start)
        bot.add_event_handler('message', self.on_message)

        bot.register_plugin('xep_0045')
        self._muc = bot.plugin['xep_0045']
        bot.register_plugin('xep_0199')
        bot.plugin['xep_0199'].enable_keepalive(30, 30)
        resource = 'sweetiewatch' + self.randomstr()

        self.mq = mq

        if not bot.connect(): raise 'could not connect'
        bot.process()

        self._bot = bot
Ejemplo n.º 3
0
class WhistlerBot(object):
    """Main Whistler bot class.

    The main WhistlerBot class handle the bot behaviour and perform subcall
    to specific command handler when a command is received in a configured
    MUC channel.

    """

    def __init__(self, jid, password, server=None, rooms=None,
            resource=None, mention=None, log=None, users=[], use_tls=False, ignore_ssl_cert=True):
        """Initialize a Whistler bot.

        Create a new :class:`WhistlerBot` object, the :func:`__init__`
        receive the following parameters:

        :param `jid`: a valid JID atom to identify the bot user.
        :param `password`: a plaintext password to identify the bot user.
        :param `server`: a tuple in the form *server*, *port* which sepcify
            the host to connect to, if not provided the server then use the
            JID domain instead.
        :param `rooms`: a :class:`list` of rooms to be autojoin in.
        :param `resource`: the XMPP resource string, or autogenerated one if
            not provided.
        :param `log`: a :class:`WhistlerLog` to log bot messages to, or
            *stdout* if none is provided.
        :param `users`: a :class:`set` of valid JID as strings which
            identify master users.
        :param use_tls: if set to true, try to use TLS where available
        :param ignore_ssl_cert: if set to false raise an exception when
            certificate do not match with hostname or any other kind of
            invalid certificate.
        """
        self.user = jid
        self.jid = jid
        self.password = password
        self.server = server
        self.log = log or WhistlerLog()
        self.use_tls = use_tls
        self.ignore_ssl_cert = ignore_ssl_cert
        self._initial_users = users
        self.handlers = { EVENT_CONNECT:  [], EVENT_DISCONNECT:  [],
                          EVENT_REGISTER: [], EVENT_JOIN:        [],
                          EVENT_LEAVE:    [], EVENT_UNREGISTER:  [],
                          EVENT_MESSAGE:  [], EVENT_MUC_MESSAGE: [],
                          EVENT_MUC_COMMAND: [], EVENT_CHANGE_STATUS: []}

        self.client = None

        self.resource = resource or self.__class__.__name__.lower() + \
                                    str(random.getrandbits(32))
        self.mention = mention or self.resource

        self.jid += "/" + self.resource
        self._rooms = set(rooms or [])

    @property
    def users(self):
        """Users in the bot roster (administrators)

        A property which return an iterator over users in bot roster, that is
        administrative users or valid users to admin the bot.

        """
        return filter(lambda x:x not in self._rooms,
                self.client.roster[self.user].keys())

    @property
    def roster(self):
        """Bot roster"""
        return self.client.roster

    @property
    def rooms(self):
        """Return a list of rooms where bot are joined in."""
        return self.client["xep_0045"].rooms.keys()

    def run_handler(self, event, *args, **kwargs):
        """Performs the handler actions related with specified event."""

        for handler in self.handlers[event]:
            handler(*args, **kwargs)

    def register_handler(self, typ, fun):
        """Register a new handler to whistler. The handler is a function
        which will be executed on certain actions.

        :param `typ`: The type of the handler, can be one of following:
            * connect: *on connect* handler,
            * disconnect: *on disconnect* handler,
            * register: *on register user* handler.
        :param `fun`: a function to be executed, which receive almost
            an instance of class :class:`WhistlerBot` as parameter. The
            register type also receive a JID in string notation of the
            registered user.
        """
        self.handlers[typ].append(fun)

    def unregister_handler(self, typ, fun):
        """Unregister a previously registerd handler."""
        self.handlers[typ].remove(fun)

    def set_subject(self, room, subject):
        """Set a new subject on specified room."""

        if room in self._rooms:
            mesg = "Whistler set subject to: %s" % subject
            self.client.send_message(room, mesg, subject, "groupchat")

    def send(self, to, mesg, typ="chat", subject=None):
        self.client.send_message(to, mesg, subject, typ)

    def register_plugin(self, plugin_name):
        """Register a new SleekXMPP plugin."""

        if not self.client:
            raise WhistlerError("No client connected")

        self.client.register_plugin(plugin_name)

    def handle_invalid_cert(self, pem_cert):
        """Handle a invalid certification request."""
        self.log.warning("Invalid certificated found, but continue anyway.")

    def connect(self):
        """Perform a connection to the server.

        This function is designed to work internally, but calls to connect
        handlers when connection is sucessful.

        """
        if self.client:
            return self.client

        self.client = ClientXMPP(self.jid, self.password)

        # Install event handlers
        self.client.add_event_handler("groupchat_message", self.handle_muc_message)
        self.client.add_event_handler("session_start", self.handle_session_start)
        self.client.add_event_handler("message", self.handle_message)
        self.client.add_event_handler("changed_status", self.handle_changed_status)

        if self.ignore_ssl_cert:
            self.client.add_event_handler("ssl_invalid_cert", self.handle_invalid_cert)

        # Add plug-ins
        self.client.register_plugin("xep_0030") # Service Discovery
        self.client.register_plugin("xep_0004") # Data Forms
        self.client.register_plugin("xep_0060") # PubSub
        self.client.register_plugin("xep_0199") # XMPP Ping
        self.client.register_plugin("xep_0045") # Multi-User Chat

        if len(self.server) == 2:
            domain, port = self.server
            self.client._expected_server_name = domain

        if self.client.connect(self.server or ()):
            if self.use_tls:
                self.log.info("starting TLS...")
                self.client.start_tls()
            self.log.info("connected to %s, port %d" % self.server)
        else:
            raise WhistlerConnectionError("Unable to connect to %s:%d"
                    % self.server)

        self.run_handler(EVENT_CONNECT)

        return self.client

    def handle_changed_status(self, presence):
        self.run_handler(EVENT_CHANGE_STATUS, presence)

    def handle_session_start(self, event):
        self.client.get_roster()
        self.client.send_presence()
        [self.join_room(room) for room in self._rooms]
        for user in self._initial_users:
            self.register_user(user)

    def register_command(self, cmdname, cmdfun):
        """Register a new command.

        This function in intended to provide a way to add commands
        on-the-fly, when :class:`WhistlerBot` is alreay instanced.

        :param `cmdname`: a name to this command.
        :param `cmdfun`: a callback which can accept three arguments, which
            will be usd when command called.

        """
        setattr(self, "cmd_%s" % cmdname, cmdfun)

    def start(self):
        """Start bot operation.

        Connect to the XMPP server and start the bot, it will be serving
        requests until the stopping is requested, using :func:`stop`
        function.

        """
        if not self.connect():
            raise WhistlerConnectionError("unknown error")

        self.client.process(threaded=False)

    def stop(self):
        """Stop bot operation.

        Stop serving requests. This function also destroys the current
        connection, if existed.

        """
        self.disconnect()

    def is_validuser(self, jid):
        """Check for whether an user is valid.

        Check whether the specified user is registered as valid user in the
        bot, according to :func:`register_user` and :func:`unregister_user`
        functions.

        """
        return (jid not in self._rooms and
                jid in self.client.roster[self.user].keys()) or \
               (jid in self._initial_users)

    def register_user(self, jid, subscription="both"):
        """Register an user as valid user for the bot."""

        self.client.update_roster(jid, subscription=subscription)
        self.run_handler(EVENT_REGISTER, jid)

    def unregister_user(self, jid, subscription="remove"):
        """Unregister an user as valid user for the bot."""

        self.client.update_roster(jid, subscription=subscription)
        self.run_handler(EVENT_UNREGISTER, jid)

    def reply(self, to, message):
        """Send a reply to an specified message, using a new message one.

        :param `to`: a message to be replied
        :param `message`: a string with the reply.

        """
        to.reply(message).send()

    def get_room_nicks(self, room):
        """Return a dict with the nicks in room as keys. Values contains
        data about these nicks."""
        return self.client.plugin["xep_0045"].rooms[room]

    def handle_muc_message(self, message):
        """Handle any received group chat message.

        :param message: Message received from the MUC room.

        """
        body = message["body"]

        self.run_handler(EVENT_MUC_MESSAGE, message, None)

        if not body or (body[0] != COMMAND_CHAR \
                and not body.startswith(self.mention + ", ") \
                and not body.startswith(self.mention + ": ") \
                and not body.startswith("@" + self.mention)):
            # None to handle
            return

        if body[0] == COMMAND_CHAR:
            command_n = body.split()[0][1:]
            arguments = body.split()[1:]
        else:
            command_n = body.split()[1]
            arguments = body.split()[2:]

        command = getattr(self, "cmd_%s" % command_n, None)
        message.command = command

        if command:
            self.log.info("muc command %s %r" % (command_n, arguments))
            result = command(message, arguments)
            if result is not None:
                if isinstance(result, list):
                    for r in result:
                        self.reply(message, r)
                elif isinstance(result, basestring):
                    self.reply(message, result)
        self.run_handler(EVENT_MUC_COMMAND, message, arguments)

    def handle_message(self, message):
        """Handle a direct chat message.

        :param message: Message received from some user.

        """
        if not message["body"]:
            return

        self.run_handler(EVENT_MESSAGE, message, None)

        if message["type"] == "groupchat":
            # discard muc messages here.
            return

        body = message["body"].split()

        command_n = body[0]
        arguments = body[1:]

        command = getattr(self, "cmd_%s" % command_n, None)

        if command:
            self.log.info("chat command %s %r" % (command_n, arguments))
            result = command(message, arguments)
            if result is not None:
                if isinstance(result, list):
                    for r in result:
                        self.reply(message, r)
                elif isinstance(result, basestring):
                    self.reply(message, result)


    def join(self, rooms, resource=None):
        """Join several rooms at once.

        :param rooms: Iterable with room JIDs as strings.
        :param resource: If given, nickname (resource) used in rooms.

        """
        [self.join_room(room, resource) for room in rooms]


    def leave(self, rooms, resource=None):
        """Leave several rooms at once.

        :param rooms: Iterable with rooms JIDs as strings.
        :param resource: If given, nickname (resource) used in rooms.

        """
        [self.leave_room(room, resource) for room in rooms]


    def join_room(self, room, resource=None):
        """Join a Multi-User Chat (MUC) room.

        Make the bot join a MUC room. If a nick different from the resource
        name is to be used, it can be specified. This allows for several
        bots to be in the same room.

        :param `room`: The room name.
        :param `resource`: A resource name for the bot in the room.
        """
        self.client.plugin["xep_0045"].joinMUC(room, resource or self.resource, maxhistory="1")
        self.run_handler(EVENT_JOIN, room)


    def disconnect(self):
        """Disconnect from the server.

        Leave all rooms, sets bot presence to unavailable, and closes the
        connection to the server.

        """
        self.log.info("Shutting down the bot...")
        self.run_handler(EVENT_DISCONNECT)

        self.client.disconnect()


    def leave_room(self, room, resource=None):
        """Leaves a Multi-User Chat (MUC) room.

        :param `room`: the room name to leave.
        :param `resource`: the resource which leaves.

        """
        self.client.plugin["xep_0045"].leaveMUC(room, resource or self.resource)
        self.run_handler(EVENT_LEAVE, room)
Ejemplo n.º 4
0
class WhistlerBot(object):
    """Main Whistler bot class.

    The main WhistlerBot class handle the bot behaviour and perform subcall
    to specific command handler when a command is received in a configured
    MUC channel.

    """

    def __init__(self, jid, password, server=None, rooms=None,
            resource=None, mention=None, log=None, users=[], use_tls=False, ignore_ssl_cert=True):
        """Initialize a Whistler bot.

        Create a new :class:`WhistlerBot` object, the :func:`__init__`
        receive the following parameters:

        :param `jid`: a valid JID atom to identify the bot user.
        :param `password`: a plaintext password to identify the bot user.
        :param `server`: a tuple in the form *server*, *port* which sepcify
            the host to connect to, if not provided the server then use the
            JID domain instead.
        :param `rooms`: a :class:`list` of rooms to be autojoin in.
        :param `resource`: the XMPP resource string, or autogenerated one if
            not provided.
        :param `log`: a :class:`WhistlerLog` to log bot messages to, or
            *stdout* if none is provided.
        :param `users`: a :class:`set` of valid JID as strings which
            identify master users.
        :param use_tls: if set to true, try to use TLS where available
        :param ignore_ssl_cert: if set to false raise an exception when
            certificate do not match with hostname or any other kind of
            invalid certificate.
        """
        self.user = jid
        self.jid = jid
        self.password = password
        self.server = server
        self.log = log or WhistlerLog()
        self.use_tls = use_tls
        self.ignore_ssl_cert = ignore_ssl_cert
        self._initial_users = users
        self.handlers = { EVENT_CONNECT:  [], EVENT_DISCONNECT:  [],
                          EVENT_REGISTER: [], EVENT_JOIN:        [],
                          EVENT_LEAVE:    [], EVENT_UNREGISTER:  [],
                          EVENT_MESSAGE:  [], EVENT_MUC_MESSAGE: [],
                          EVENT_MUC_COMMAND: [], EVENT_CHANGE_STATUS: []}

        self.client = None

        self.resource = resource or self.__class__.__name__.lower() + \
                                    str(random.getrandbits(32))
        self.mention = mention or self.resource

        self.jid += "/" + self.resource
        self._rooms = set(rooms or [])

    @property
    def users(self):
        """Users in the bot roster (administrators)

        A property which return an iterator over users in bot roster, that is
        administrative users or valid users to admin the bot.

        """
        return filter(lambda x:x not in self._rooms,
                self.client.roster[self.user].keys())

    @property
    def roster(self):
        """Bot roster"""
        return self.client.roster

    @property
    def rooms(self):
        """Return a list of rooms where bot are joined in."""
        return self.client["xep_0045"].rooms.keys()

    def run_handler(self, event, *args, **kwargs):
        """Performs the handler actions related with specified event."""

        for handler in self.handlers[event]:
            handler(*args, **kwargs)

    def register_handler(self, typ, fun):
        """Register a new handler to whistler. The handler is a function
        which will be executed on certain actions.

        :param `typ`: The type of the handler, can be one of following:
            * connect: *on connect* handler,
            * disconnect: *on disconnect* handler,
            * register: *on register user* handler.
        :param `fun`: a function to be executed, which receive almost
            an instance of class :class:`WhistlerBot` as parameter. The
            register type also receive a JID in string notation of the
            registered user.
        """
        self.handlers[typ].append(fun)

    def unregister_handler(self, typ, fun):
        """Unregister a previously registerd handler."""
        self.handlers[typ].remove(fun)

    def set_subject(self, room, subject):
        """Set a new subject on specified room."""

        if room in self._rooms:
            mesg = "Whistler set subject to: %s" % subject
            self.client.send_message(room, mesg, subject, "groupchat")

    def send(self, to, mesg, typ="chat", subject=None):
        self.client.send_message(to, mesg, subject, typ)

    def register_plugin(self, plugin_name):
        """Register a new SleekXMPP plugin."""

        if not self.client:
            raise WhistlerError("No client connected")

        self.client.register_plugin(plugin_name)

    def handle_invalid_cert(self, pem_cert):
        """Handle a invalid certification request."""
        self.log.warning("Invalid certificated found, but continue anyway.")

    def connect(self):
        """Perform a connection to the server.

        This function is designed to work internally, but calls to connect
        handlers when connection is sucessful.

        """
        if self.client:
            return self.client

        self.client = ClientXMPP(self.jid, self.password)

        # Install event handlers
        self.client.add_event_handler("groupchat_message", self.handle_muc_message)
        self.client.add_event_handler("session_start", self.handle_session_start)
        self.client.add_event_handler("message", self.handle_message)
        self.client.add_event_handler("changed_status", self.handle_changed_status)

        if self.ignore_ssl_cert:
            self.client.add_event_handler("ssl_invalid_cert", self.handle_invalid_cert)

        # Add plug-ins
        self.client.register_plugin("xep_0030") # Service Discovery
        self.client.register_plugin("xep_0004") # Data Forms
        self.client.register_plugin("xep_0060") # PubSub
        self.client.register_plugin("xep_0199") # XMPP Ping
        self.client.register_plugin("xep_0045") # Multi-User Chat

        if self.client.connect(self.server or ()):
            if self.use_tls:
                self.log.info("starting TLS...")
                self.client.start_tls()
            self.log.info("connected to %s, port %d" % self.server)
        else:
            raise WhistlerConnectionError("Unable to connect to %s:%d"
                    % self.server)

        self.run_handler(EVENT_CONNECT)

        return self.client

    def handle_changed_status(self, presence):
        self.run_handler(EVENT_CHANGE_STATUS, presence)

    def handle_session_start(self, event):
        self.client.get_roster()
        self.client.send_presence()
        [self.join_room(room) for room in self._rooms]
        for user in self._initial_users:
            self.register_user(user)

    def register_command(self, cmdname, cmdfun):
        """Register a new command.

        This function in intended to provide a way to add commands
        on-the-fly, when :class:`WhistlerBot` is alreay instanced.

        :param `cmdname`: a name to this command.
        :param `cmdfun`: a callback which can accept three arguments, which
            will be usd when command called.

        """
        setattr(self, "cmd_%s" % cmdname, cmdfun)

    def start(self):
        """Start bot operation.

        Connect to the XMPP server and start the bot, it will be serving
        requests until the stopping is requested, using :func:`stop`
        function.

        """
        if not self.connect():
            raise WhistlerConnectionError("unknown error")

        self.client.process(threaded=False)

    def stop(self):
        """Stop bot operation.

        Stop serving requests. This function also destroys the current
        connection, if existed.

        """
        self.disconnect()

    def is_validuser(self, jid):
        """Check for whether an user is valid.

        Check whether the specified user is registered as valid user in the
        bot, according to :func:`register_user` and :func:`unregister_user`
        functions.

        """
        return (jid not in self._rooms and
                jid in self.client.roster[self.user].keys()) or \
               (jid in self._initial_users)

    def register_user(self, jid, subscription="both"):
        """Register an user as valid user for the bot."""

        self.client.update_roster(jid, subscription=subscription)
        self.run_handler(EVENT_REGISTER, jid)

    def unregister_user(self, jid, subscription="remove"):
        """Unregister an user as valid user for the bot."""

        self.client.update_roster(jid, subscription=subscription)
        self.run_handler(EVENT_UNREGISTER, jid)

    def reply(self, to, message):
        """Send a reply to an specified message, using a new message one.

        :param `to`: a message to be replied
        :param `message`: a string with the reply.

        """
        to.reply(message).send()

    def get_room_nicks(self, room):
        """Return a dict with the nicks in room as keys. Values contains
        data about these nicks."""
        return self.client.plugin["xep_0045"].rooms[room]

    def handle_muc_message(self, message):
        """Handle any received group chat message.

        :param message: Message received from the MUC room.

        """
        body = message["body"]

        self.run_handler(EVENT_MUC_MESSAGE, message, None)

        if not body or (body[0] != COMMAND_CHAR \
                and not body.startswith(self.mention + ", ") \
                and not body.startswith(self.mention + ": ") \
                and not body.startswith("@" + self.mention)):
            # None to handle
            return

        if body[0] == COMMAND_CHAR:
            command_n = body.split()[0][1:]
            arguments = body.split()[1:]
        else:
            command_n = body.split()[1]
            arguments = body.split()[2:]

        command = getattr(self, "cmd_%s" % command_n, None)
        message.command = command

        if command:
            self.log.info("muc command %s %r" % (command_n, arguments))
            result = command(message, arguments)
            if result is not None:
                if isinstance(result, list):
                    for r in result:
                        self.reply(message, r)
                elif isinstance(result, basestring):
                    self.reply(message, result)
        self.run_handler(EVENT_MUC_COMMAND, message, arguments)

    def handle_message(self, message):
        """Handle a direct chat message.

        :param message: Message received from some user.

        """
        if not message["body"]:
            return

        self.run_handler(EVENT_MESSAGE, message, None)

        if message["type"] == "groupchat":
            # discard muc messages here.
            return

        body = message["body"].split()

        command_n = body[0]
        arguments = body[1:]

        command = getattr(self, "cmd_%s" % command_n, None)

        if command:
            self.log.info("chat command %s %r" % (command_n, arguments))
            result = command(message, arguments)
            if result is not None:
                if isinstance(result, list):
                    for r in result:
                        self.reply(message, r)
                elif isinstance(result, basestring):
                    self.reply(message, result)


    def join(self, rooms, resource=None):
        """Join several rooms at once.

        :param rooms: Iterable with room JIDs as strings.
        :param resource: If given, nickname (resource) used in rooms.

        """
        [self.join_room(room, resource) for room in rooms]


    def leave(self, rooms, resource=None):
        """Leave several rooms at once.

        :param rooms: Iterable with rooms JIDs as strings.
        :param resource: If given, nickname (resource) used in rooms.

        """
        [self.leave_room(room, resource) for room in rooms]


    def join_room(self, room, resource=None):
        """Join a Multi-User Chat (MUC) room.

        Make the bot join a MUC room. If a nick different from the resource
        name is to be used, it can be specified. This allows for several
        bots to be in the same room.

        :param `room`: The room name.
        :param `resource`: A resource name for the bot in the room.
        """
        self.client.plugin["xep_0045"].joinMUC(room, resource or self.resource, maxhistory="1")
        self.run_handler(EVENT_JOIN, room)


    def disconnect(self):
        """Disconnect from the server.

        Leave all rooms, sets bot presence to unavailable, and closes the
        connection to the server.

        """
        self.log.info("Shutting down the bot...")
        self.run_handler(EVENT_DISCONNECT)

        self.client.disconnect()


    def leave_room(self, room, resource=None):
        """Leaves a Multi-User Chat (MUC) room.

        :param `room`: the room name to leave.
        :param `resource`: the resource which leaves.

        """
        self.client.plugin["xep_0045"].leaveMUC(room, resource or self.resource)
        self.run_handler(EVENT_LEAVE, room)