async def main(): # TODO: this really needs to be replaced # probably using https://docs.python.org/3.8/library/functools.html#functools.partial global client global plugin_loader # Read config file config = Config("config.yaml") # Configure the database store = Storage(config.database_filepath) # Configuration options for the AsyncClient client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, encryption_enabled=config.enable_encryption, ) # Initialize the matrix client client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, store_path=config.store_filepath, config=client_config, ) # instantiate the pluginLoader plugin_loader = PluginLoader() # Set up event callbacks callbacks = Callbacks(client, store, config, plugin_loader) client.add_event_callback(callbacks.message, (RoomMessageText, )) client.add_event_callback(callbacks.invite, (InviteEvent, )) client.add_event_callback(callbacks.event_unknown, (UnknownEvent, )) client.add_response_callback(run_plugins) # Keep trying to reconnect on failure (with some time in-between) error_retries: int = 0 while True: try: # Try to login with the configured username/password try: login_response = await client.login( password=config.user_password, device_name=config.device_name, ) # Check if login failed if type(login_response) == LoginError: logger.error( f"Failed to login: {login_response.message}, retrying in 15s... ({error_retries})" ) # try logging in a few times to work around temporary login errors during homeserver restarts if error_retries < 3: error_retries += 1 await sleep(15) continue else: return False else: error_retries = 0 except LocalProtocolError as e: # There's an edge case here where the user enables encryption but hasn't installed # the correct C dependencies. In that case, a LocalProtocolError is raised on login. # Warn the user if these conditions are met. if config.enable_encryption: logger.fatal( "Failed to login and encryption is enabled. Have you installed the correct dependencies? " "https://github.com/poljar/matrix-nio#installation") return False else: # We don't know why this was raised. Throw it at the user logger.fatal(f"Error logging in: {e}") # Login succeeded! # Sync encryption keys with the server # Required for participating in encrypted rooms if client.should_upload_keys: await client.keys_upload() logger.info(f"Logged in as {config.user_id}") await client.sync_forever(timeout=30000, full_state=True) except (ClientConnectionError, ServerDisconnectedError, AttributeError, asyncio.TimeoutError) as err: logger.debug(err) logger.warning( f"Unable to connect to homeserver, retrying in 15s...") # Sleep so we don't bombard the server with login requests await sleep(15) finally: # Make sure to close the client connection on disconnect await client.close()
async def main(): """The first function that is run when starting the bot""" # Read user-configured options from a config file. # A different config file path can be specified as the first command line argument if len(sys.argv) > 1: config_path = sys.argv[1] else: config_path = "config.yaml" # Read the parsed config file and create a Config object config = Config(config_path) # Configure the database store = Storage(config.database) # Configuration options for the AsyncClient client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, encryption_enabled=True, ) # Initialize the matrix client client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, store_path=config.store_path, config=client_config, ) if config.user_token: client.access_token = config.user_token client.user_id = config.user_id # Set up event callbacks callbacks = Callbacks(client, store, config) client.add_event_callback(callbacks.invite, (InviteMemberEvent, )) client.add_event_callback(callbacks.decryption_failure, (MegolmEvent, )) client.add_response_callback(callbacks.sync, (SyncResponse, )) client.add_event_callback(callbacks.unknown, (UnknownEvent, )) # Keep trying to reconnect on failure (with some time in-between) while True: try: if config.user_token: # Use token to log in client.load_store() # Sync encryption keys with the server if client.should_upload_keys: await client.keys_upload() else: # Try to login with the configured username/password try: login_response = await client.login( password=config.user_password, device_name=config.device_name, ) # Check if login failed if type(login_response) == LoginError: logger.error("Failed to login: %s", login_response.message) return False except LocalProtocolError as e: # There's an edge case here where the user hasn't installed the correct C # dependencies. In that case, a LocalProtocolError is raised on login. logger.fatal( "Failed to login. Have you installed the correct dependencies? " "https://github.com/poljar/matrix-nio#installation " "Error: %s", e, ) return False # Login succeeded! logger.info(f"Logged in as {config.user_id}") # join the pins room we'll be writing to result = client.join(config.pins_room) if type(result) == JoinError: raise Exception( f"Error joining pins room {config.pins_room}", ) await client.sync_forever(timeout=30000, full_state=True) except Exception as e: logger.error("%s", e) logger.warning( "Unable to connect to homeserver, retrying in 15s...") # Sleep so we don't bombard the server with login requests sleep(15) finally: # Make sure to close the client connection on disconnect try: await client.close() except Exception as e2: logger.error("Also got exception while closing, %s", e2)
class E2EEClient: def __init__(self, join_rooms: set): self.STORE_PATH = os.environ['LOGIN_STORE_PATH'] self.CONFIG_FILE = f"{self.STORE_PATH}/credentials.json" self.join_rooms = join_rooms self.client: AsyncClient = None self.client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, encryption_enabled=True, ) self.greeting_sent = False def _write_details_to_disk(self, resp: LoginResponse, homeserver) -> None: with open(self.CONFIG_FILE, "w") as f: json.dump( { 'homeserver': homeserver, # e.g. "https://matrix.example.org" 'user_id': resp.user_id, # e.g. "@user:example.org" 'device_id': resp.device_id, # device ID, 10 uppercase letters 'access_token': resp.access_token # cryptogr. access token }, f ) async def _login_first_time(self) -> None: homeserver = os.environ['MATRIX_SERVER'] user_id = os.environ['MATRIX_USERID'] pw = os.environ['MATRIX_PASSWORD'] device_name = os.environ['MATRIX_DEVICE'] if not os.path.exists(self.STORE_PATH): os.makedirs(self.STORE_PATH) self.client = AsyncClient( homeserver, user_id, store_path=self.STORE_PATH, config=self.client_config, ssl=(os.environ['MATRIX_SSLVERIFY'] == 'True'), ) resp = await self.client.login(password=pw, device_name=device_name) if (isinstance(resp, LoginResponse)): self._write_details_to_disk(resp, homeserver) else: logging.info( f"homeserver = \"{homeserver}\"; user = \"{user_id}\"") logging.critical(f"Failed to log in: {resp}") sys.exit(1) async def _login_with_stored_config(self) -> None: if self.client: return with open(self.CONFIG_FILE, "r") as f: config = json.load(f) self.client = AsyncClient( config['homeserver'], config['user_id'], device_id=config['device_id'], store_path=self.STORE_PATH, config=self.client_config, ssl=bool(os.environ['MATRIX_SSLVERIFY']), ) self.client.restore_login( user_id=config['user_id'], device_id=config['device_id'], access_token=config['access_token'] ) async def login(self) -> None: if os.path.exists(self.CONFIG_FILE): logging.info('Logging in using stored credentials.') else: logging.info('First time use, did not find credential file.') await self._login_first_time() logging.info( f"Logged in, credentials are stored under '{self.STORE_PATH}'.") await self._login_with_stored_config() async def _message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: logging.info(colored( f"@{room.user_name(event.sender)} in {room.display_name} | {event.body}", 'green' )) async def _sync_callback(self, response: SyncResponse) -> None: logging.info(f"We synced, token: {response.next_batch}") if not self.greeting_sent: self.greeting_sent = True greeting = f"Hi, I'm up and runnig from **{os.environ['MATRIX_DEVICE']}**, waiting for webhooks!" await self.send_message(greeting, os.environ['MATRIX_ADMIN_ROOM'], 'Webhook server') async def send_message( self, message: str, room: str, sender: str, sync: Optional[bool] = False ) -> None: if sync: await self.client.sync(timeout=3000, full_state=True) msg_prefix = "" if os.environ['DISPLAY_APP_NAME'] == 'True': msg_prefix = f"**{sender}** says: \n" content = { 'msgtype': 'm.text', 'body': f"{msg_prefix}{message}", } if os.environ['USE_MARKDOWN'] == 'True': # Markdown formatting removes YAML newlines if not padded with spaces, # and can also mess up posted data like system logs logging.debug('Markdown formatting is turned on.') content['format'] = 'org.matrix.custom.html' content['formatted_body'] = markdown( f"{msg_prefix}{message}", extensions=['extra']) await self.client.room_send( room_id=room, message_type="m.room.message", content=content, ignore_unverified_devices=True ) async def run(self) -> None: await self.login() self.client.add_event_callback(self._message_callback, RoomMessageText) self.client.add_response_callback(self._sync_callback, SyncResponse) if self.client.should_upload_keys: await self.client.keys_upload() for room in self.join_rooms: await self.client.join(room) await self.client.joined_rooms() logging.info('The Matrix client is waiting for events.') await self.client.sync_forever(timeout=300000, full_state=True)
class Bot: commands = {} allowed_users = {} cfg = cfg sync_delay = 1000 user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:60.9) ' 'Gecko/20100101 Goanna/4.4 Firefox/60.9 PaleMoon/28.7.2' def __init__(self, loglevel=None): config = ClientConfig(encryption_enabled=True, pickle_key=cfg.pickle_key, store_name=cfg.store_name, store_sync_tokens=True) if not os.path.exists(cfg.store_path): os.makedirs(cfg.store_path) self.http_session = aiohttp.ClientSession( headers={'User-Agent': self.user_agent}) self.client = AsyncClient( cfg.server, cfg.user, cfg.device_id, config=config, store_path=cfg.store_path ) logger_group.level = getattr( logbook, loglevel) if loglevel else logbook.CRITICAL logbook.StreamHandler(sys.stdout).push_application() self.logger = logbook.Logger('bot') logger_group.add_logger(self.logger) self.mli = MessageLinksInfo(self.http_session) self._register_commands() self.client.add_response_callback(self._sync_cb, SyncResponse) self.client.add_response_callback( self._key_query_cb, KeysQueryResponse) self.client.add_event_callback(self._invite_cb, InviteMemberEvent) def _preserve_name(self, path): return path.split('/')[-1].split('.py')[0].strip().replace(' ', '_').replace('-', '') def _validate_module(self, module): return hasattr(module, 'handler') and callable(module.handler) def _process_module(self, module): name = self._preserve_name(module.name) if hasattr(module, 'name') and isinstance( module.name, str) else module.__name__ handler = module.handler raw_aliases = module.aliases if hasattr(module, 'aliases') and \ (isinstance(module.aliases, tuple) or isinstance(module.aliases, str)) else () raw_aliases = raw_aliases if isinstance( raw_aliases, tuple) else [raw_aliases] aliases = () for alias in raw_aliases: alias = self._preserve_name(alias.replace('%', '')) aliases = (*aliases, alias) help = module.help if hasattr(module, 'help') and \ isinstance(module.help, str) else '' return (name, handler, aliases, help) def _register_commands(self): files_to_import = [fn for fn in glob.glob( "./commands/*.py") if not fn.count('__')] for file_path in files_to_import: try: name = self._preserve_name(file_path) spec = importlib.util.spec_from_file_location( name, file_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) name = module.name if hasattr(module, 'name') else name if not self._validate_module(module): raise ImportError(f'Unable to register command \'{name}\' ' f'(\'{module.__file__}\'). ' 'Command module must contain a callable ' 'object with name \'handler\'.') command = Command( *self._process_module(module), self) if not self.commands.get(command.name): self.commands[command.name] = command else: raise ImportError( f'Unable to register command \'{command.name}\' ' f'(\'{module.__file__}\'). ' 'A command with this name already exists.') for alias in command.aliases: if not self.commands.get(alias): self.commands[alias] = command else: self.logger.warn(f'Unable to register alias \'{alias}\'! ' 'An alias with this name already exists ' f'({self.commands[alias]}). Ignoring.') except Exception as e: self.logger.critical(e) if self.commands: self.logger.info( f'Registered commands: {list(set(self.commands.values()))}') else: self.logger.warn('No commands added!') def _parse_command(self, message): match = re.findall(r'^%([\w\d_]*)\s?(.*)$', message) if match: return (match[0][0], (match[0][1].split())) else: return (None, None) async def _serve_forever(self): response = await self.client.login(cfg.password) self.logger.info(response) await self.client.sync_forever(1000, full_state=True) async def _key_query_cb(self, response): for device in self.client.device_store: if device.trust_state.value == 0: if device.user_id in cfg.manager_accounts: self.client.verify_device(device) self.logger.info( f'Verified manager\'s device {device.device_id} for user {device.user_id}') else: self.client.blacklist_device(device) async def _invite_cb(self, room, event): if room.room_id not in self.client.rooms and \ event.sender in cfg.manager_accounts: await self.client.join(room.room_id) self.logger.info( f'Accepted invite to room {room.room_id} from {event.sender}') def _is_sender_verified(self, sender): devices = [ d for d in self.client.device_store.active_user_devices(sender)] return all(map(lambda d: d.trust_state.value == 1, devices)) async def _process_links(self, message, room_id): info = await self.mli._get_info(message) if info: nl = '\n' content = { 'body': f'{nl.join(info)}', 'formatted_body': f'{nl.join(map(lambda i: i.join(["<strong>", "</strong>"]),info))}', 'format': 'org.matrix.custom.html', 'msgtype': 'm.text' } await self.client.room_send(room_id, 'm.room.message', content) async def _sync_cb(self, response): if len(response.rooms.join) > 0: joins = response.rooms.join for room_id in joins: for event in joins[room_id].timeline.events: if self._is_sender_verified(event.sender) and hasattr(event, 'body'): command, args = self._parse_command(event.body) if command and command in self.commands: await self.commands[command].run( args, event, room_id) self.logger.debug( f'serving command \'{command}\' with arguments {args} in room {room_id}') if event.sender != self.cfg.user: await self._process_links(event.body, room_id) def serve(self): loop = asyncio.get_event_loop() loop.run_until_complete(self._serve_forever())
class Session: client: AsyncClient = None config: SessionConfig = None plugins: Dict[str, BasePlugin] = None messenger: Messenger = None loggers: List[Logger] = [] def __init__(self, config: SessionConfig): self.config = config self.client = AsyncClient(config.homeserver, config.matrix_id) try: with open(config.next_batch_file, "r") as next_batch_token: self.client.next_batch = next_batch_token.read() except FileNotFoundError: # No existing next_batch file; no worries. self.client.next_batch = 0 # Update next_batch every sync self.client.add_response_callback(self.__sync_cb, SyncResponse) # Handle text messages self.client.add_event_callback(self.__message_cb, RoomMessageText) # Handle invites self.client.add_event_callback(self.__autojoin_room_cb, InviteEvent) self.client.add_ephemeral_callback(self.sample, ReceiptEvent) self.load_plugins() self.messenger = Messenger(self.client) async def sample(self, room: MatrixRoom, event: ReceiptEvent) -> None: CORE_LOG.info(room.read_receipts) async def start(self) -> None: """Start the session. Logs in as the user provided in the config and begins listening for events to respond to. For security, it will logout any other sessions. """ login_status = await self.client.login( password=self.config.password, device_name="remote-bot" ) if isinstance(login_status, LoginError): print(f"Failed to login: {login_status}", file=sys.stderr) await self.stop() else: # Remove previously registered devices; ignore this device maybe_devices = await self.client.devices() if isinstance(maybe_devices, DevicesResponse): await self.client.delete_devices( list( map( lambda x: x.id, filter( lambda x: x.id != self.client.device_id, maybe_devices.devices, ), ) ), auth={ "type": "m.login.password", "user": self.config.matrix_id, "password": self.config.password, }, ) CORE_LOG.info(login_status) # Force a full state sync to load Room info await self.client.sync(full_state=True) await self.client.sync_forever(timeout=30000) async def stop(self) -> None: """Politely closes the session and ends the process. If logged in, logs out. """ print("Shutting down...") if self.client.logged_in: await self.client.logout() await self.client.close() sys.exit(0) def load_plugins(self) -> None: """Dynamically loads all plugins from the plugins directory. New plugins can be added by creating new classes in the `plugins` module. """ self.plugins = {} importlib.import_module("plugins") modules = [] plugin_files = os.listdir(os.path.join(os.path.dirname(__file__), "plugins")) if len(plugin_files) == 0: print("NOTE: No plugin files found.") for plugin in plugin_files: if plugin.startswith("__") or not plugin.endswith(".py"): # Skip files like __init__.py and .gitignore continue module_name = "plugins." + plugin.rsplit(".")[0] modules.append(importlib.import_module(module_name, package="plugins")) for module in modules: if module.__name__ in sys.modules: importlib.reload(module) clsmembers = inspect.getmembers( module, lambda member: inspect.isclass(member) and member.__module__ == module.__name__, ) for name, cls in clsmembers: if not issubclass(cls, BasePlugin): # We only want plugins that derive from BasePlugin CORE_LOG.warn( f"Skipping {name} as it doesn't derive from the BasePlugin" ) continue CORE_LOG.info(f"Loading plugin {name} ...") # Create logger for each plugin plugin_logger = Logger(f"olive.plugin.{name}") plugin_logger.info(f"{name}'s logger is working hard!") logger_group.add_logger(plugin_logger) # Generate standard config config = PluginConfig(plugin_logger) # Instantiate the plugin! self.plugins[name] = cls(config) CORE_LOG.info("Loaded plugins") async def __send( self, room: MatrixRoom, body: str = None, content: dict = None ) -> bool: # You must either include a body message or build your own content dict. assert body or content if not content: content = {"msgtype": "m.text", "body": body} try: send_status = await self.client.room_send( room.room_id, message_type="m.room.message", content=content ) except SendRetryError as err: print(f"Failed to send message '{body}' to room '{room}'. Error:\n{err}") return False if isinstance(send_status, RoomSendError): print(send_status) return False return True async def __autojoin_room_cb( self, room: MatrixInvitedRoom, event: InviteEvent ) -> None: if room.room_id not in self.client.rooms: await self.client.join(room.room_id) await self.__send(room, f"Hello, {room.display_name}!") # TODO: Replace forced client sync. I'd like to avoid handling the # Invite event three times, but dont' want to force syncs. Probably # not common enought to matter? await self.client.sync(300) async def __message_cb(self, room: MatrixRoom, event: RoomMessageText): """Executes any time a MatrixRoom the bot is in receives a RoomMessageText. On each message, it tests each plugin to see if it is triggered by the event; if so, the method will run that plugins `process_event` method. """ await self.client.room_read_markers( room.room_id, event.event_id, event.event_id ) if event.sender == self.client.user_id: # Message is from us; we can ignore. return for name, plugin in self.plugins.items(): try: await plugin.process_event(room, event, self.messenger) except Exception as err: print( f"Plugin {name} encountered an error while " + f"processing the event {event} in room {room.display_name}." + f"\n{err}", file=sys.stderr, ) _, _, tb = sys.exc_info() traceback.print_tb(tb) async def __sync_cb(self, response: SyncResponse) -> None: with open(self.config.next_batch_file, "w") as next_batch_token: next_batch_token.write(response.next_batch)
class LainBot: _initial_sync_done = False def __init__(self, config_path): self.config = None self.loop = asyncio.get_event_loop() self.scheduler = schedule.default_scheduler with open(config_path, "r") as cfg_file: self.config = yaml.safe_load(cfg_file) if self.config is None: sys.exit(13) log_file = self.config["bot"]["log_file"] formatter = "%(asctime)s; %(levelname)s; %(message)s" logging.basicConfig(filename=log_file, level=logging.DEBUG, format=formatter) self.logger = logging.getLogger("LainBot") self.logger.info("Initializing system.") self.logger.info("Start client.") self.homeserver = self.config["bot"]["host"] self.access_token = self.config["bot"]["token"] self.user_id = self.config["bot"]["username"] self.user_pw = self.config["bot"]["password"] self.bot_owners = self.config["bot"]["owners"] self.device_id = self.config["bot"]["device_name"] self.room_id = self.config["bot"]["room_id"] self.path = self.config["bot"]["pics_path"] self.event_time = self.config["bot"]["event_time"] self.client = None self.http_client = None self.users = list() self.logger.info("Register job.") self.scheduler.every().day.at(self.event_time).do(self.job) self.loop.create_task(self.timer()) self.logger.info("Initializing system complete.") async def on_error(self, response): self.logger.error(response) if self.client: self.logger.error("closing client") await self.client.close() sys.exit(1) async def on_sync(self, _response): if not self._initial_sync_done: self._initial_sync_done = True for room in self.client.rooms: self.logger.info('room %s', room) self.logger.info('initial sync done, ready for work') async def start(self): self.logger.info("Initializing client.") self.client = AsyncClient(self.homeserver) self.client.access_token = self.access_token self.client.user_id = self.user_id self.client.device_id = self.device_id self.logger.info("Initializing http client.") self.http_client = HttpClient(self.homeserver) self.logger.info("Register callbacks.") self.client.add_response_callback(self.on_error, SyncError) self.client.add_response_callback(self.on_sync, SyncResponse) # self.client.add_event_callback(self.on_invite, InviteMemberEvent) self.client.add_event_callback(self.on_message, RoomMessageText) self.client.add_event_callback(self.on_unknown, UnknownEvent) self.client.add_event_callback(self.on_image, RoomMessageImage) self.logger.info("Starting initial sync") await self.client.sync_forever(timeout=30000) async def timer(self): # Timer function that runs pending jobs in scheduler, # Is meant to be run in clients event loop by calling # client.loop.create_task(self.timer()) while True: await self.scheduler.run_pending() await asyncio.sleep(1) async def job(self): self.logger.info("Job started") pic_list = os.listdir(self.path) pic_num = randint(a=0, b=len(pic_list) - 1) pic = pic_list[pic_num] self.logger.info(f"Upload {pic}") pic_path = os.path.join(self.path, pic) await self.send_image(pic_path) self.logger.info("Job finished") self.users.clear() return async def send_image(self, image): """Send image to to matrix. Arguments: --------- client : Client room_id : str image : str, file name of image This is a working example for a JPG image. "content": { "body": "someimage.jpg", "info": { "size": 5420, "mimetype": "image/jpeg", "thumbnail_info": { "w": 100, "h": 100, "mimetype": "image/jpeg", "size": 2106 }, "w": 100, "h": 100, "thumbnail_url": "mxc://example.com/SomeStrangeThumbnailUriKey" }, "msgtype": "m.image", "url": "mxc://example.com/SomeStrangeUriKey" } """ mime_type = magic.from_file(image, mime=True) # e.g. "image/jpeg" if not mime_type.startswith("image/"): self.logger.info("Drop message because file does not have an image mime type.") return im = Image.open(image) (width, height) = im.size # im.size returns (width,height) tuple # first do an upload of image, then send URI of upload to room file_stat = await aiofiles.os.stat(image) async with aiofiles.open(image, "r+b") as f: resp, maybe_keys = await self.client.upload( f, content_type=mime_type, # image/jpeg filename=os.path.basename(image), filesize=file_stat.st_size) if isinstance(resp, UploadResponse): self.logger.info("Image was uploaded successfully to server. ") else: self.logger.info(f"Failed to upload image. Failure response: {resp}") content = { "body": os.path.basename(image), # descriptive title "info": { "size": file_stat.st_size, "mimetype": mime_type, "thumbnail_info": None, # TODO "w": width, # width in pixel "h": height, # height in pixel "thumbnail_url": None, # TODO }, "msgtype": "m.image", "url": resp.content_uri, } try: await self.client.room_send( self.room_id, message_type="m.room.message", content=content ) self.logger.info("Image was sent successfully") except Exception as e: self.logger.debug(e) self.logger.info(f"Image send of file {image} failed.") async def on_message(self, room, event): await self.client.update_receipt_marker(room.room_id, event.event_id) if not self._initial_sync_done: return if event.sender == self.client.user_id: return msg = event.body if msg.startswith("!"): if msg[1:] == "pic": if event.sender in self.users: return self.logger.debug("picture for {0}: {1}".format(event.sender, event.body)) self.users.append(event.sender) pic_list = os.listdir(self.path) pic_num = randint(a=0, b=len(pic_list) - 1) pic = pic_list[pic_num] pic_path = os.path.join(self.path, pic) await self.send_image(pic_path) elif msg[1:] == "hello": await self.client.room_typing(room.room_id, True) await self.client.room_send(room.room_id, message_type="m.room.message", content="hello") await self.client.room_typing(room.room_id, False) return async def on_image(self, room, event): if not self._initial_sync_done: return self.logger.debug(f"Image received in room {room.display_name}\n{room.user_name(event.sender)} | {event.body}") async def on_unknown(self, room, event): if not self._initial_sync_done: return room_id = room.room_id self.logger.debug(f"room_id = {room_id}") self.logger.debug(f"event = {event}") if event.type == "m.reaction": if event.source['content']['m.relates_to']['key'] == '👍️': self.logger.debug("EVENT KEY") self.logger.debug(f"User {event.sender} Key {event.source['content']['m.relates_to']['key']}") message_event_id = event.source['content']['m.relates_to']['event_id'] event_id = event.source['event_id'] self.logger.debug(f"Event ID: {event_id} - Reaction ID {message_event_id}") msg = await self.client.room_get_event(room_id=room_id, event_id=message_event_id) # self.logger.debug("MSG") # self.logger.debug(msg) # # self.logger.debug("MSG Transport") # self.logger.debug(msg.transport_response) self.logger.debug("Client Response") json_data = await self.client.parse_body(msg.transport_response) self.logger.debug("JSON Response") self.logger.debug(json_data) self.logger.debug("Response Type") self.logger.debug(json_data.get('type')) if json_data["type"] == 'm.room.message': sender = json_data.get('sender') if sender not in self.bot_owners: return content = json_data.get('content') if content.get('msgtype') == 'm.image': mxc = content.get('url') server_name = urlparse(mxc).netloc media_id = os.path.basename(urlparse(mxc).path) self.logger.debug(f"MXC = {mxc}") self.logger.debug(f"Server = {server_name}") self.logger.debug(f"Media ID = {media_id}") try: image = await self.client.download(server_name=server_name, media_id=media_id, filename=None, allow_remote=True) assert isinstance(image, DownloadResponse) filename = image.filename body = image.body self.logger.debug(f"filename = {filename}") path = os.path.join(self.path, filename) with open(path, 'wb') as image_file: image_file.write(body) image_file.close() self.logger.debug("Image download success") await self.client.room_typing(room_id, True) await self.client.room_send(room_id, message_type="m.room.message", content=f"Image {filename} saved!") await self.client.room_typing(room_id, False) except Exception as e: self.logger.debug(e) await self.client.room_typing(room_id, True) await self.client.room_send(room_id, message_type="m.room.message", content=f"Ops, Something wrong!") await self.client.room_typing(room_id, False)