Exemple #1
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())
Exemple #2
0
async def main():  # noqa
    """Create bot as Matrix client and enter event loop."""
    # Read config file
    # A different config file path can be specified
    # as the first command line argument
    if len(sys.argv) > 1:
        config_filepath = sys.argv[1]
    else:
        config_filepath = "config.yaml"
    config = Config(config_filepath)

    # 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=True,
    )

    # 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,
    )

    # Set up event callbacks
    callbacks = Callbacks(client, store, config)
    client.add_event_callback(callbacks.message, (RoomMessageText, ))
    client.add_event_callback(callbacks.invite, (InviteMemberEvent, ))
    client.add_to_device_callback(callbacks.accept_all_verify,
                                  (KeyVerificationEvent, ))

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            # Try to login with the configured username/password
            try:
                if config.access_token:
                    logger.debug("Using access token from config file to log "
                                 f"in. access_token={config.access_token}")

                    client.restore_login(user_id=config.user_id,
                                         device_id=config.device_id,
                                         access_token=config.access_token)
                else:
                    logger.debug("Using password from config file to log in.")
                    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: "******"{login_response.message}")
                        return False
                    logger.info((f"access_token of device {config.device_name}"
                                 f" is: \"{login_response.access_token}\""))
            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? "
                    "Error: %s", e)
                return False

            # Login succeeded!
            logger.debug(f"Logged in successfully as user {config.user_id} "
                         f"with device {config.device_id}.")
            # Sync encryption keys with the server
            # Required for participating in encrypted rooms
            if client.should_upload_keys:
                await client.keys_upload()

            if config.change_device_name:
                content = {"display_name": config.device_name}
                resp = await client.update_device(config.device_id, content)
                if isinstance(resp, UpdateDeviceError):
                    logger.debug(f"update_device failed with {resp}")
                else:
                    logger.debug(f"update_device successful with {resp}")

            await client.sync(timeout=30000, full_state=True)
            for device_id, olm_device in client.device_store[
                    config.user_id].items():
                logger.info("Setting up trust for my own "
                            f"device {device_id} and session key "
                            f"{olm_device.keys}.")
                client.verify_device(olm_device)

            await client.sync_forever(timeout=30000, full_state=True)

        except (ClientConnectionError, ServerDisconnectedError):
            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
            await client.close()
Exemple #3
0
class Bot():
    def __init__(self, controller, creds):
        self.name = controller.BOTKIT_BOT_NAME
        self.CHANNEL_GREETING = controller.CHANNEL_GREETING
        self.user = creds['user']
        self.password = creds['password']
        client_args = {}
        if hasattr(controller, 'CLIENT_ARGS'):
            client_args = controller.CLIENT_ARGS
        self.client = AsyncClient(creds['homeserver'], creds['user'],
                                  **client_args)
        self.client.add_event_callback(self.invite_cb, InviteMemberEvent)
        self.client.add_event_callback(self.message_cb, RoomMessageText)
        self._setup_handlers(controller)
        if hasattr(controller, 'AUTH'):
            self.AUTH = controller.AUTH(controller)
        else:
            self.AUTH = BlockAll(controller)
        if hasattr(controller, 'CACHE'):
            self.CACHE = controller.CACHE
        else:
            self.CACHE = NoCache()

    def _setup_handlers(self, controller):
        self.prefix = controller.BOTKIT_BOT_PREFIX
        if not self.prefix:
            self.prefix = "!" + self.name
        self.commands = {}
        self.msg_handler = None
        self.startup_method = None
        members = inspect.getmembers(controller, predicate=inspect.ismethod)
        for member in members:
            if hasattr(member[1], 'botkit_method'):
                # add member[1]
                command_str = f'{self.prefix} {member[0]}'
                self.commands[command_str] = member[1]
            elif hasattr(member[1], 'botkit_msg_handler'):
                if self.msg_handler:
                    raise Exception('Can only mark one botkit_msg_handler!')
                self.msg_handler = member[1]
            elif hasattr(member[1], 'botkit_startup_method'):
                if self.startup_method:
                    raise Exception('Can only mark one botkit_startup_method!')
                self.startup_method = member[1]
        command_prefixes = '|'.join(list(self.commands.keys()))
        self.command_regex = re.compile(f'^({command_prefixes})( .+)?$')

    async def message_cb(self, room, event):
        if not self.AUTH.authenticate_message(room, event):
            return
        await self._trust_room(room)
        txt = event.body.strip()
        context = {'room': room, 'event': event, 'client': self.client}
        match = self.command_regex.match(txt)
        if match:
            await self._handle_command(match, context)
        elif self.msg_handler:
            try:
                await self.msg_handler(context)
            except:
                return

    async def _trust_room(self, room):
        if self.client.room_contains_unverified(room_id=room.room_id):
            for user_id in self.client.rooms[room.room_id].users:
                print(f"Checking {user_id}")
                if not self.client.olm.user_fully_verified(user_id):
                    # device store and that requires syncing with the server.
                    for device_id, olm_device in self.client.device_store[
                            user_id].items():
                        self.client.verify_device(olm_device)
                        print(f"Trusting {device_id} from user {user_id}")

    async def _handle_command(self, match, context):
        room = context.get('room')
        event = context.get('event')
        full_request = match.group(0)
        command_str = match.group(1)
        command = self.commands[command_str]
        use_cache = hasattr(command, 'cache_result')
        content = None
        if use_cache:
            content = self.CACHE.get_result(room, event)
        if not content:
            args = []
            args_str = match.group(2)
            if args_str:
                args = shlex.split(args_str)
            content = await self._execute_command(command, args, context)
        if content:
            if use_cache:
                self.CACHE.set_result(content, room, event)
            await self.client.room_send(room_id=room.room_id,
                                        message_type='m.room.message',
                                        content=content)

    async def _execute_command(self, command, args, context):
        try:
            res = await command(*args, context=context)
            if res:
                content = await res.get_content(client=self.client)
                return content
        except:
            return None

    async def invite_cb(self, room, event):
        if not self.AUTH.authenticate_invite(room, event):
            return
        if event.membership == 'invite' and event.state_key == self.user:
            await self.client.join(room.room_id)
            greeting = self._get_greeting()
            await self.client.room_send(room_id=room.room_id,
                                        message_type='m.room.message',
                                        content={
                                            'msgtype': 'm.text',
                                            "format": "org.matrix.custom.html",
                                            'body': greeting,
                                            'formatted_body': greeting
                                        })

    def _get_greeting(self):
        if self.CHANNEL_GREETING:
            return self.CHANNEL_GREETING
        msg = f"<h1>Hello!, I'm {self.name}.</h1><h3>Try some commands:</h3><ul>"
        command_strings = list(self.commands.keys())
        for cmd in command_strings:
            msg = msg + f'<li><code>{cmd}</code></li>'
        msg = msg + '</ul>'
        return msg

    async def loginandsync(self):
        await self.client.login(self.password)
        if self.startup_method:
            await self.startup_method(self.client)
        await self.client.sync_forever(timeout=30000)

    def run(self):
        asyncio.get_event_loop().run_until_complete(self.loginandsync())