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()
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)
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" )
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")