class MatrixBackend(ErrBot):
    def __init__(self, config):
        super().__init__(config)

        if not hasattr(config, 'MATRIX_HOMESERVER'):
            log.fatal("""
            You need to specify a homeserver to connect to in
            config.MATRIX_HOMESERVER.

            For example:
            MATRIX_HOMESERVER = "https://matrix.org"
            """)
            sys.exit(1)

        self._homeserver = config.MATRIX_HOMESERVER
        self._username = config.BOT_IDENTITY['username']
        self._password = config.BOT_IDENTITY['password']

    def serve_once(self):
        self.connect_callback()

        try:
            self._client = MatrixClient(self._homeserver)
            self._token = self._client.register_with_password(
                self._username,
                self._password,
            )
        except MatrixRequestError:
            log.fatal("""
            Incorrect username or password specified in
            config.BOT_IDENTITY['username'] or config.BOT_IDENTITY['password'].
            """)
            sys.exit(1)

        try:
            while True:
                time.sleep(2)
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down...")
            return True
        finally:
            self.disconnect_callback()

    def rooms(self):
        rooms = []
        raw_rooms = self._client.get_rooms()

        for rid, robject in raw_rooms:
            # TODO: Get the canonical alias rather than the first one from
            #       `Room.aliases`.
            log.debug('Found room %s (aka %s)' % (rid, rid.aliases[0]))

    def send_message(self, mess):
        super().send_message(mess)

    def connect_callback(self):
        super().connect_callback()
Beispiel #2
0
class Client:
    """
    A Matrix client implementation.

    :param server_url: The Matrix server URL
    :type server_url: str
    :param UI: The user interface object
    :type UI: :class:`.ui.base.BaseUI`
    """
    def __init__(self, server_url, UI):
        assert server_url, "Missing server URL"

        self.room = None

        self.client = MatrixClient(server_url)

        self.op_executor = OPExecutor(self._server_exception_handler)

        self.room_event_observer = RoomEventObserver(self)

        self.users = Users(self.client.api)

        commands = {}
        for name in dir(self):
            attr = getattr(self, name)
            if isinstance(attr, command.Command):
                commands[attr.cmd_type] = attr
        self.ui = UI(self.send_message, self.users, commands)

        self.users.set_modified_callback(self.ui.refresh_user_list)

    @property
    def connected(self):
        return self.client and self.client.should_listen

    def register(self, username, password):
        """
        Register a new user on the server.

        :param username: The username to register
        :type username: str
        :param password: The password to register
        :type password: str
        :raises RegistrationException:
        """
        assert username, "Missing username"
        assert password, "Missing password"

        LOG.info("Registering user {}".format(username))

        try:
            self.client.register_with_password(username=username,
                                               password=password)
        except MatrixRequestError as exc:
            LOG.exception(exc)

            try:
                content = json.loads(exc.content)
            except json.decoder.JSONDecodeError as json_exc:
                raise exceptions.RegistrationException(str(json_exc))

            try:
                if content["errcode"] in ("M_USER_IN_USE", "M_EXCLUSIVE"):
                    raise exceptions.UsernameTaken(username)

                if content["errcode"] == "M_INVALID_USERNAME":
                    raise exceptions.RegistrationException(content["error"])

                if content["errcode"] == "M_UNKNOWN":
                    if content["error"] == "Captcha is required.":
                        raise exceptions.CaptchaRequired()
            except KeyError:
                pass

            raise exceptions.RegistrationUnknownError(exc)

    def login(self, username, password):
        """
        Login to the server.

        If the login fails we try to register a new user using the same
        username and password.

        :param username: The username to login with
        :type username: str
        :param password: The password to login with
        :type password: str
        :raises LoginException:
        """
        assert username, "Missing username"
        assert password, "Missing password"

        LOG.info("Login with username {}".format(username))

        try:
            self.client.login_with_password(username=username,
                                            password=password)
        except MatrixRequestError as exc:
            LOG.exception(exc)

            try:
                content = json.loads(exc.content)
            except json.decoder.JSONDecodeError as json_exc:
                raise exceptions.LoginException(str(json_exc))

            try:
                if content["errcode"] == "M_FORBIDDEN":
                    raise exceptions.LoginFailed()
            except KeyError:
                pass

            raise exceptions.LoginUnknownError(exc)

    def create_room(self, room_alias):
        """
        Create a new room on the server.

        :param room_alias: The alias of the room to create
        :type room_alias: str
        """
        assert room_alias, "Missing room"

        LOG.info("Creating room {}".format(room_alias))
        """ #room:host -> room """
        room_alias_name = room_alias[1:].split(':')[0]

        self.room = self.client.create_room(room_alias_name)

    def join(self, room_alias):
        """
        Join a room.

        If the room does not already exist on the server we try to
        automatically create it.

        :param room_alias: The alias of the room to join
        :type room_alias: str
        :raises JoinRoomException:
        """
        assert room_alias, "Missing room"

        LOG.info("Joining room {}".format(room_alias))

        try:
            self.room = self.client.join_room(room_alias)
        except MatrixRequestError as exc:
            LOG.exception(exc)

            try:
                content = json.loads(exc.content)
            except json.decoder.JSONDecodeError as json_exc:
                exceptions.JoinRoomException(json_exc)

            try:
                if content["errcode"] == "M_NOT_FOUND":
                    raise exceptions.RoomNotFound()

                if content["errcode"] in ("M_FORBIDDEN", "M_UNKNOWN"):
                    raise exceptions.JoinRoomException(content["error"])
            except (KeyError, AttributeError):
                pass

            raise exceptions.JoinRoomUnknownError(exc)

    def run(self):
        """
        Run the client.
        """
        assert self.room, "You need to join a room before you run the client"

        self.room.add_listener(self.room_event_observer.on_room_event)

        self.op_executor.start()

        self.connect()

        self.ui.run()

    def stop(self):
        """
        Stop the client.
        """
        self.ui.stop()

        self.op_executor.stop()

        if self.connected:
            print("Waiting for server connection to close")
            print("Press ctrl+c to force stop")
            try:
                self.disconnect()
            except KeyboardInterrupt:
                pass

    def _server_exception_handler(self, exc):
        """
        Exception handler for Matrix server errors.
        """
        LOG.exception(exc)

        if isinstance(exc, ConnectionError):
            self.ui.draw_client_info("Server connection error")
        else:
            self.ui.draw_client_info("Unexpected server error: {}".format(exc))
            if not settings.debug:
                self.ui.draw_client_info(
                    "For more details enable debug mode, "
                    "reproduce the issue and check the logs. "
                    "Debug mode is enabled by setting the "
                    "MATRIX_DEBUG environment variable")

        self.disconnect()

    def _populate_room(self):
        # Clear the users model from old user data. To avoid duplicates when
        # for example reconnecting
        self.users.clear()

        # Temporarily disable UI user list refresh callback when doing the
        # initial population of the users model. Especially important for large
        # rooms which would cause an excessive amount of re-draws.
        modified_callback = self.users.modified_callback
        self.users.set_modified_callback(lambda: None)

        users = self.room.get_joined_members().items()
        for user_id, user in users:
            self.users.add_user(user_id, nick=user["displayname"])

        # Restore refresh callback
        self.users.set_modified_callback(modified_callback)

        # Execute an initial refresh
        modified_callback()

    @command.cmd(command.CONNECT, help_msg="Reconnect to the server")
    @op(require_connection=False)
    def connect(self):
        """
        Connect to the server.

        Before the client starts syncing events with the server it retrieves
        the users currently in the room and backfills previous messages. The
        number of previous messages backfilled are decidede by
        :const:`HISTORY_LIMIT`.
        """
        if self.connected:
            self.ui.draw_client_info("Already connected")
            return

        # Retrieve users currently in the room
        self._populate_room()

        # Get message history
        self.room.backfill_previous_messages(limit=HISTORY_LIMIT)

        self.client.start_listener_thread(
            timeout_ms=SERVER_TIMEOUT_MS,
            exception_handler=self._server_exception_handler)

        self.ui.draw_client_info("Connected to server")

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

        This also causes the user to logout when the sync thread has closed.
        """
        if self.connected:
            # Can't unfortunately cancel an in-flight request
            # https://github.com/kennethreitz/requests/issues/1353
            self.client.should_listen = False

            self.ui.draw_client_info("Disconnected")

            # Wait here for the matrix client sync thread to exit before
            # joining so that we can interrupt using signals.
            while self.client.sync_thread.isAlive():
                time.sleep(0.1)

        try:
            self.client.api.logout()
        except ConnectionError:
            pass

    @op
    def send_message(self, msg):
        """
        Send a message to the room.

        :param msg: Message to send
        :type msg: str
        """
        self.room.send_text(msg)

    @command.cmd(command.INVITE,
                 help_msg=("Invite a user to the room "
                           "(user_id syntax: @[mxid]:[server])"))
    @op
    def invite(self, user_id):
        """
        Invite a user to the room.

        :param user_id: The MXID of the user you want to invite
        :type user_id: str
        """
        try:
            self.client.api.invite_user(self.room.room_id, user_id)
            # self.room.invite_user(args[0])
        except MatrixRequestError as exc:
            try:
                error_msg = json.loads(exc.content)["error"]
            except:
                error_msg = str(exc)
            self.ui.draw_client_info("Invite error: {}".format(error_msg))

    @command.cmd(command.CHANGE_NICK, help_msg="Change nick")
    @op
    def change_nick(self, nick):
        """
        Change your nick.

        :param nick: The displayname you want to change to.
        :type nick: str
        """
        self.users.get_user(self.client.user_id).set_display_name(nick)

    @command.cmd(command.LEAVE, help_msg="Leave the room")
    def leave(self):
        """
        Leave the room.
        """
        # Stop listening for new events, when we leave the room we're forbidden
        # from interacting with the it anything anyway
        self.client.should_listen = False

        self.room.leave()

        self.ui.stop()

    @command.cmd(command.QUIT, help_msg="Exit the client")
    def quit(self):
        """
        Exit the client.
        """
        self.ui.stop()

    @command.cmd(command.HELP, help_msg="Show this")
    def show_help(self, cmd_type=None):
        """
        Show a help message explaining all the available commands.

        :param cmd_type: Use cmd_type to display the help message of a specific
                         command rather than all of them.
        :type cmd_type: int
        """
        self.ui.draw_help(cmd_type)
Beispiel #3
0
class MatrixBackend(ErrBot):
    def __init__(self, config):
        super().__init__(config)

        if not hasattr(config, 'MATRIX_HOMESERVER'):
            log.fatal("""
            You need to specify a homeserver to connect to in
            config.MATRIX_HOMESERVER.

            For example:
            MATRIX_HOMESERVER = "https://matrix.org"
            """)
            sys.exit(1)

        self._homeserver = config.MATRIX_HOMESERVER
        self._username = config.BOT_IDENTITY['username']
        self._password = config.BOT_IDENTITY['password']
        self._api = None
        self._token = None


    def serve_once(self):
        def dispatch_event(event):
            log.info("Received event: %s" % event)

            if event['type'] == "m.room.member":
                if event['membership'] == "invite" and event['state_key'] == self._client.user_id:
                    room_id = event['room_id']
                    self._client.join_room(room_id)
                    log.info("Auto-joined room: %s" % room_id)

            if event['type'] == "m.room.message" and event['sender'] != self._client.user_id:
                sender = event['sender']
                room_id = event['room_id']
                body = event['content']['body']
                log.info("Received message from %s in room %s" % (sender, room_id))

                # msg = Message(body)
                # msg.frm = MatrixPerson(self._client, sender, room_id)
                # msg.to = MatrixPerson(self._client, self._client.user_id, room_id)
                # self.callback_message(msg) 

                msg = self.build_message(body)
                room = MatrixRoom(room_id)
                msg.frm = MatrixRoomOccupant(self._api, room, sender)
                msg.to = room
                self.callback_message(msg) 

        self.reset_reconnection_count()
        self.connect_callback()

        self._client = MatrixClient(self._homeserver)

        try:
            self._token = self._client.register_with_password(self._username,
                                                              self._password,)
        except MatrixRequestError as e:
            if e.code == 400 or e.code == 403:
                try:
                    self._token = self._client.login_with_password(self._username,
                                                     self._password,)
                except MatrixRequestError:
                    log.fatal("""
                        Incorrect username or password specified in
                        config.BOT_IDENTITY['username'] or config.BOT_IDENTITY['password'].
                    """)
                    sys.exit(1)

        self._api = MatrixHttpApi(self._homeserver, self._token)

        self.bot_identifier = MatrixPerson(self._api)

        self._client.add_listener(dispatch_event)

        try:
            while True:
                self._client.listen_for_events()
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down...")
            return True
        finally:
            self.disconnect_callback()

    def rooms(self):
        rooms = []
        raw_rooms = self._client.get_rooms()

        for rid, robject in raw_rooms:
            # TODO: Get the canonical alias rather than the first one from
            #       `Room.aliases`.
            log.debug('Found room %s (aka %s)' % (rid, rid.aliases[0]))

    def send_message(self, mess):
        super().send_message(mess)

        room_id = mess.to.room.id
        text_content = item_url = mess.body

        if item_url.startswith("http://") or item_url.startswith("https://"):
            if item_url.endswith("gif"):
                self._api.send_content(room_id, item_url, "image", "m.image")
                return

        # text_content = Markdown().convert(mess.body)
        self._api.send_message(room_id, text_content)

    def connect_callback(self):
        super().connect_callback()

    def build_identifier(self, txtrep):
        raise Exception(
            "XXX"
        )

    def build_reply(self, mess, text=None, private=False):
        log.info("build_reply")

        response = self.build_message(text)
        response.frm = self.bot_identifier
        response.to = mess.frm
        return response

    def change_presence(self, status: str = '', message: str = ''):
        raise Exception(
            "XXX"
        )

    @property
    def mode(self):
        return 'matrix'

    def query_room(self, room):
        raise Exception(
            "XXX"
        )
Beispiel #4
0
class MatrixBackend(ErrBot):
    def __init__(self, config):
        super().__init__(config)

        if not hasattr(config, 'MATRIX_HOMESERVER'):
            log.fatal("""
            You need to specify a homeserver to connect to in
            config.MATRIX_HOMESERVER.

            For example:
            MATRIX_HOMESERVER = "https://matrix.org"
            """)
            sys.exit(1)

        self._homeserver = config.MATRIX_HOMESERVER
        self._username = config.BOT_IDENTITY['username']
        self._password = config.BOT_IDENTITY['password']
        self._api = None
        self._token = None

    def serve_once(self):
        def dispatch_event(event):
            log.info("Received event: %s" % event)

            if event['type'] == "m.room.member":
                if event['membership'] == "invite" and event[
                        'state_key'] == self._client.user_id:
                    room_id = event['room_id']
                    self._client.join_room(room_id)
                    log.info("Auto-joined room: %s" % room_id)

            if event['type'] == "m.room.message" and event[
                    'sender'] != self._client.user_id:
                sender = event['sender']
                room_id = event['room_id']
                body = event['content']['body']
                log.info("Received message from %s in room %s" %
                         (sender, room_id))

                # msg = Message(body)
                # msg.frm = MatrixPerson(self._client, sender, room_id)
                # msg.to = MatrixPerson(self._client, self._client.user_id, room_id)
                # self.callback_message(msg)

                msg = self.build_message(body)
                room = MatrixRoom(room_id)
                msg.frm = MatrixRoomOccupant(self._api, room, sender)
                msg.to = room
                self.callback_message(msg)

        self.reset_reconnection_count()
        self.connect_callback()

        self._client = MatrixClient(self._homeserver)

        try:
            self._token = self._client.register_with_password(
                self._username,
                self._password,
            )
        except MatrixRequestError as e:
            if e.code == 400:
                try:
                    self._token = self._client.login_with_password(
                        self._username,
                        self._password,
                    )
                except MatrixRequestError:
                    log.fatal("""
                        Incorrect username or password specified in
                        config.BOT_IDENTITY['username'] or config.BOT_IDENTITY['password'].
                    """)
                    sys.exit(1)

        self._api = MatrixHttpApi(self._homeserver, self._token)

        self.bot_identifier = MatrixPerson(self._api)

        self._client.add_listener(dispatch_event)

        try:
            while True:
                self._client.listen_for_events()
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down...")
            return True
        finally:
            self.disconnect_callback()

    def rooms(self):
        rooms = []
        raw_rooms = self._client.get_rooms()

        for rid, robject in raw_rooms:
            # TODO: Get the canonical alias rather than the first one from
            #       `Room.aliases`.
            log.debug('Found room %s (aka %s)' % (rid, rid.aliases[0]))

    def send_message(self, mess):
        super().send_message(mess)

        room_id = mess.to.room.id
        text_content = item_url = mess.body

        if item_url.startswith("http://") or item_url.startswith("https://"):
            if item_url.endswith("gif"):
                self._api.send_content(room_id, item_url, "image", "m.image")
                return

        # text_content = Markdown().convert(mess.body)
        self._api.send_message(room_id, text_content)

    def connect_callback(self):
        super().connect_callback()

    def build_identifier(self, txtrep):
        raise Exception("XXX")

    def build_reply(self, mess, text=None, private=False):
        log.info("build_reply")

        response = self.build_message(text)
        response.frm = self.bot_identifier
        response.to = mess.frm
        return response

    def change_presence(self, status: str = '', message: str = ''):
        raise Exception("XXX")

    @property
    def mode(self):
        return 'matrix'

    def query_room(self, room):
        raise Exception("XXX")