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