Пример #1
0
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()
Пример #2
0
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)
Пример #4
0
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())
Пример #5
0
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)
Пример #6
0
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)