class Bot: def __init__(self): self.client = AsyncClient(settings.MATRIX_SERVER, settings.MATRIX_USERNAME) self.command_handlers = { "cbstart": self.start, "cbstats": self.cbstats, } self.client.add_event_callback(self.message_handler, RoomMessageText) self.client.add_event_callback(self.invite_handler, InviteEvent) async def run(self): await self.client.login(settings.MATRIX_PASSWORD) await self.client.sync_forever(timeout=30000) async def invite_handler(self, room: MatrixRoom, event: InviteEvent): await self.client.join(room.room_id) async def message_handler(self, room, event: RoomMessageText): # Set up command handlers args = event.body.split(" ") if len(args) > 0 and args[0].startswith("!"): command = args[0][1:] if command in self.command_handlers: await self.command_handlers[command](room, event) async def send_message(self, room, message): 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": message, "formatted_body": message, }, ) async def start(self, room, _): await self.send_message(room, "Running") async def cbstats(self, room, event): args = event.body.split(" ")[1:] message = "" country_info = None try: if len(args) == 0: stats, updated = data.get_global_cases() else: country = " ".join(args) stats, country_info, updated = data.get_country_cases(country) last_updated = datetime.fromtimestamp(int(updated) / 1000) message = formatting.format_stats(stats, country_info, last_updated) except JSONDecodeError: if country is not None: message = f"{country} doesn't exist lmao" else: message = "Error: Could not look up stats" await self.send_message(room, message)
async def main(): # Read config file config = Config("config.yaml") # Configuration options for the AsyncClient nio_client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, ) # Initialize the matrix client nio_client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, config=nio_client_config, store_path="/home/brendan/Documents/matrix-monzo-next/nio_store", ) # Initialise the monzo client monzo_client = Monzo(config.monzo_access_token) await nio_client.login(config.password, "monzo_bot") # Set up event callbacks callbacks = Callbacks(config, nio_client, monzo_client) nio_client.add_event_callback(callbacks.message, (RoomMessageText, )) nio_client.add_event_callback(callbacks.invite, (InviteEvent, )) # First do a sync with full_state = true to retrieve the state of the room. await nio_client.sync(full_state=True) await nio_client.sync_forever(30000)
class MatrixClient: def __init__(self, bot_funcs): self.bot_funcs = bot_funcs self.client = AsyncClient(URI, USERNAME) self.last_sent = 0 print("init finished") async def message_callback(self, room: MatrixRoom, event: RoomMessageText): for f in self.bot_funcs: if event.body.startswith(f["trigger"]): ts = time.time() if ts - self.last_sent < 2: continue self.last_sent = time.time() res = f["ret_func"](event.body, event.sender) if res is None: continue await self.client.room_send(room_id=ROOM, message_type="m.room.message", content={ "msgtype": "m.text", "body": res }) async def exec_client(self): self.client.add_event_callback(self.message_callback, RoomMessageText) print(await self.client.login(PW)) await self.client.sync_forever(timeout=30000) # milliseconds
async def init(homeserver: str, username: str, password: str, keyfile: Optional[str], keyphrase: Optional[str]) -> AsyncClient: config = AsyncClientConfig(store=SqliteMemoryStore, store_sync_tokens=True) client = AsyncClient(homeserver, username, config=config) response = await client.login(password) client.add_event_callback(event_cb, RoomMessage) await client.import_keys(keyfile, keyphrase) return client
async def main(): urbitClient.connect() matrixClient = AsyncClient(matrixHomeServer, matrixBotUser) print(await matrixClient.login(matrixBotPass)) matrixClient.add_event_callback(matrixTextListener, RoomMessageText) matrixClient.add_event_callback(matrixMediaListener, RoomMessageMedia) await matrixClient.sync_forever(timeout=30000)
async def main() -> None: client = AsyncClient("https://matrix.ether.ai", os.environ['BOT_USER']) client.add_event_callback(message_callback, RoomMessageText) print(await client.login(os.environ['BOT_PASSWORD'])) # "Logged in as @alice:example.org device id: RANDOMDID" # If you made a new room and haven't joined as that user, you can use # await client.join("your-room-id") file_path = 'logs/myLinks.log' # Referenced from stack overflow https://stackoverflow.com/a/24818607 last_line = None with open(file_path, 'r') as f: while True: line = f.readline() if not line: break last_line = line while True: with open(file_path, 'r') as f: lines = f.readlines() if lines[-1] != last_line: last_line = lines[-1] await client.room_send( # Watch out! If you join an old room you'll see lots of old messages room_id="!nXDYFQhwioFnAwUHiB:ether.ai", message_type="m.room.message", content={ "msgtype": "m.text", "body": lines[-1] } ) time.sleep(0.00000000000001) # while True: # line = file1.readline() # # if not line: # await client.sync_forever(timeout=1000) # milliseconds # # else: # await client.room_send( # # Watch out! If you join an old room you'll see lots of old messages # room_id="!nXDYFQhwioFnAwUHiB:ether.ai", # message_type="m.room.message", # content={ # "msgtype": "m.text", # "body": line # } # ) await client.sync_forever(timeout=30000) # milliseconds
async def main(): # 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, ) # Initialize the matrix client client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, config=client_config, ) logger.debug("Connected to Matrix!") # Assign an access token to the bot instead of logging in and creating a new device client.access_token = config.access_token # Set up event callbacks callbacks = Callbacks(client, store, config) client.add_event_callback(callbacks.message, (RoomMessageText, )) client.add_event_callback(callbacks.invite, (InviteEvent, )) # Retrieve the last sync token if it exists token = store.get_sync_token() # Sync loop while True: # Sync with the server sync_response = await client.sync(timeout=30000, full_state=True, since=token) # Check if the sync had an error if type(sync_response) == SyncError: logger.warning("Error in client sync: %s", sync_response.message) continue # Save the latest sync token token = sync_response.next_batch if token: store.save_sync_token(token)
async def main() -> None: bot_info = load_bot_info() mongo_client = MongoClient( "mongodb+srv://{}:{}@{}/weekling?retryWrites=true&w=majority".format( bot_info["db_username"], bot_info["db_password"], bot_info["db_hostname"])) db = mongo_client.weekling # NOTE Hemppa-hack jointime = datetime.datetime.now() # HACKHACKHACK to avoid running old # commands after join join_hack_time = 5 # Seconds """ Create the client-object with correct info, set callbacks for reacting to events and login. If an access token is not found in config-file, ask for password. """ client = AsyncClient(bot_info["homeserver"]) client.add_event_callback(pass_to_invite_callback(client), InviteMemberEvent) client.add_event_callback( pass_to_message_callback(client, db, jointime, join_hack_time), RoomMessageText) # Ask password from command line, press enter to use stored access token access_token = bot_info["access_token"] user_id = bot_info["user_id"] if len(access_token) != 0 and len(user_id) != 0: client.access_token = access_token # Manually set user id because not calling client.login() client.user_id = user_id else: password = getpass.getpass() response = await client.login(password) # Save info to file for future use bot_info["access_token"] = response.access_token try: with io.open(LOGIN_FILE, "w", encoding="utf-8") as fp: fp.write(json.dumps(bot_info)) except OSError as e: print(f"Writing login-info failed: {e}") print(f"Logged in as {client}") await client.sync_forever(timeout=30000, full_state=False) # milliseconds
async def main() -> None: client = AsyncClient("https://matrix.example.org", "@alice:example.org") client.add_event_callback(message_callback, RoomMessageText) print(await client.login("my-secret-password")) # "Logged in as @alice:example.org device id: RANDOMDID" # If you made a new room and haven't joined as that user, you can use # await client.join("your-room-id") await client.room_send( # Watch out! If you join an old room you'll see lots of old messages room_id="!my-fave-room:example.org", message_type="m.room.message", content={ "msgtype": "m.text", "body": "Hello world!" }) await client.sync_forever(timeout=30000) # milliseconds
async def main(sugaroid: Sugaroid) -> None: config = Config.from_environment() client = AsyncClient(config['homeserver']) client.access_token = config['access_token'] client.user_id = config['user_id'] client.device_id = config['device_id'] print("Status: sleeping for 30000") await client.sync(30000) print("Resuming:") cb = Callbacks(client, sugaroid=sugaroid) client.add_event_callback(cb.message, RoomMessageText) while True: try: await client.sync_forever(timeout=30000, full_state=True) except KeyboardInterrupt: break except (ClientConnectionError, ServerDisconnectedError): print("Unable to connect to homeserver, retrying in 15s") time.sleep(15) finally: await client.close()
async def main(): global g_client print("Connecting to server: {}".format(botconfig.client_url)) g_client = AsyncClient(botconfig.client_url, f"@{botconfig.username}:cclub.cs.wmich.edu") # log in password = get_password() print(await g_client.login(password)) # figure out what time it is await room_send_text(botconfig.ROOM_ID_BOTTOY, tk.get_session_startup_string()) # register event callbacks g_client.add_event_callback(on_message, RoomMessageText) g_client.add_event_callback(on_image, RoomMessageImage) g_client.add_event_callback(on_video, RoomMessageVideo) g_client.add_event_callback(on_membership, RoomMemberEvent) # loop forever await g_client.sync_forever(timeout=120000) # two minutes
async def main(): global client access_token = os.getenv('MATRIX_ACCESS_TOKEN') join_on_invite = os.getenv('JOIN_ON_INVITE') client = AsyncClient(os.environ['MATRIX_SERVER'], os.environ['MATRIX_USER']) if access_token: client.access_token = access_token else: await client.login(os.environ['MATRIX_PASSWORD']) print("Access token:", client.access_token) await client.sync() if client.logged_in: client.add_event_callback(message_cb, RoomMessageText) client.add_event_callback(unknown_cb, RoomMessageUnknown) if join_on_invite: print('Note: Bot will join rooms if invited') client.add_event_callback(invite_cb, (InviteEvent, )) print('Bot running') await client.sync_forever(timeout=30000) else: print('Client was not able to log in, check env variables!')
class Bot: def __init__(self): self.appid = 'org.vranki.hemppa' self.version = '1.2' self.client = None self.join_on_invite = False self.modules = dict() self.pollcount = 0 self.poll_task = None self.owners = [] self.debug = os.getenv("DEBUG") self.initializeLogger() self.logger = logging.getLogger("hemppa") self.logger.debug("Initialized") def initializeLogger(self): if os.path.exists('config/logging.config'): logging.config.fileConfig('config/logging.config') else: log_format = '%(levelname)s - %(name)s - %(message)s' logging.basicConfig(format=log_format) if self.debug: logging.root.setLevel(logging.DEBUG) else: logging.root.setLevel(logging.INFO) async def send_text(self, room, body): msg = { "body": body, "msgtype": "m.text" } await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_html(self, room, html, plaintext): msg = { "msgtype": "m.text", "format": "org.matrix.custom.html", "formatted_body": html, "body": plaintext } await self.client.room_send(room.room_id, 'm.room.message', msg) def remove_callback(self, callback): for cb_object in bot.client.event_callbacks: if cb_object.func == callback: self.logger.info("remove callback") bot.client.event_callbacks.remove(cb_object) def get_room_by_id(self, room_id): return self.client.rooms[room_id] # Throws exception if event sender is not a room admin def must_be_admin(self, room, event): if not self.is_admin(room, event): raise CommandRequiresAdmin # Throws exception if event sender is not a bot owner def must_be_owner(self, event): if not self.is_owner(event): raise CommandRequiresOwner # Returns true if event's sender is admin in the room event was sent in, # or is bot owner def is_admin(self, room, event): if self.is_owner(event): return True if event.sender not in room.power_levels.users: return False return room.power_levels.users[event.sender] >= 50 # Returns true if event's sender is owner of the bot def is_owner(self, event): return event.sender in self.owners def save_settings(self): module_settings = dict() for modulename, moduleobject in self.modules.items(): try: module_settings[modulename] = moduleobject.get_settings() except Exception: traceback.print_exc(file=sys.stderr) data = {self.appid: self.version, 'module_settings': module_settings} self.set_account_data(data) def load_settings(self, data): if not data: return if not data.get('module_settings'): return for modulename, moduleobject in self.modules.items(): if data['module_settings'].get(modulename): try: moduleobject.set_settings( data['module_settings'][modulename]) except Exception: traceback.print_exc(file=sys.stderr) async def message_cb(self, room, event): # Figure out the command body = event.body if not self.starts_with_command(body): return command = body.split().pop(0) # Strip away non-alphanumeric characters, including leading ! for security command = re.sub(r'\W+', '', command) moduleobject = self.modules.get(command) if moduleobject is not None: if moduleobject.enabled: try: await moduleobject.matrix_message(bot, room, event) except CommandRequiresAdmin: await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.') except CommandRequiresOwner: await self.send_text(room, f'Sorry, only bot owner can run that command.') except Exception: await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details') traceback.print_exc(file=sys.stderr) else: self.logger.error(f"Unknown command: {command}") # TODO Make this configurable # await self.send_text(room, # f"Sorry. I don't know what to do. Execute !help to get a list of available commands.") @staticmethod def starts_with_command(body): """Checks if body starts with ! and has one or more letters after it""" return re.match(r"^!\w.*", body) is not None async def invite_cb(self, room, event): room: MatrixRoom event: InviteEvent if self.join_on_invite or self.is_owner(event): for attempt in range(3): result = await self.client.join(room.room_id) if type(result) == JoinError: self.logger.error(f"Error joining room %s (attempt %d): %s", room.room_id, attempt, result.message) else: self.logger.info(f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'") break else: self.logger.warning(f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}') def load_module(self, modulename): try: self.logger.info(f'load module: {modulename}') module = importlib.import_module('modules.' + modulename) module = reload(module) cls = getattr(module, 'MatrixModule') return cls(modulename) except ModuleNotFoundError: self.logger.error(f'Module {modulename} failed to load!') traceback.print_exc(file=sys.stderr) return None def reload_modules(self): for modulename in bot.modules: self.logger.info(f'Reloading {modulename} ..') self.modules[modulename] = self.load_module(modulename) self.load_settings(self.get_account_data()) def get_modules(self): modulefiles = glob.glob('./modules/*.py') for modulefile in modulefiles: modulename = os.path.splitext(os.path.basename(modulefile))[0] moduleobject = self.load_module(modulename) if moduleobject: self.modules[modulename] = moduleobject def clear_modules(self): self.modules = dict() async def poll_timer(self): while True: self.pollcount = self.pollcount + 1 for modulename, moduleobject in self.modules.items(): if moduleobject.enabled: try: await moduleobject.matrix_poll(bot, self.pollcount) except Exception: traceback.print_exc(file=sys.stderr) await asyncio.sleep(10) def set_account_data(self, data): userid = urllib.parse.quote(self.matrix_user) ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}" response = requests.put(ad_url, json.dumps(data)) self.__handle_error_response(response) if response.status_code != 200: self.logger.error('Setting account data failed. response: %s json: %s', response, response.json()) def get_account_data(self): userid = urllib.parse.quote(self.matrix_user) ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}" response = requests.get(ad_url) self.__handle_error_response(response) if response.status_code == 200: return response.json() self.logger.error(f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.') return None def __handle_error_response(self, response): if response.status_code == 401: self.logger.error("access token is invalid or missing") self.logger.info("NOTE: check MATRIX_ACCESS_TOKEN or set MATRIX_PASSWORD") sys.exit(2) def init(self): self.matrix_user = os.getenv('MATRIX_USER') self.matrix_pass = os.getenv('MATRIX_PASSWORD') matrix_server = os.getenv('MATRIX_SERVER') bot_owners = os.getenv('BOT_OWNERS') access_token = os.getenv('MATRIX_ACCESS_TOKEN') join_on_invite = os.getenv('JOIN_ON_INVITE') if matrix_server and self.matrix_user and bot_owners: self.client = AsyncClient(matrix_server, self.matrix_user) self.client.access_token = access_token if self.client.access_token is None: if self.matrix_pass is None: self.logger.error("Either MATRIX_ACCESS_TOKEN or MATRIX_PASSWORD need to be set") sys.exit(1) self.join_on_invite = join_on_invite is not None self.owners = bot_owners.split(',') self.get_modules() else: self.logger.error("The environment variables MATRIX_SERVER, MATRIX_USER and BOT_OWNERS are mandatory") sys.exit(1) def start(self): self.load_settings(self.get_account_data()) enabled_modules = [module for module_name, module in self.modules.items() if module.enabled] self.logger.info(f'Starting {len(enabled_modules)} modules..') for modulename, moduleobject in self.modules.items(): if moduleobject.enabled: try: moduleobject.matrix_start(bot) except Exception: traceback.print_exc(file=sys.stderr) def stop(self): self.logger.info(f'Stopping {len(self.modules)} modules..') for modulename, moduleobject in self.modules.items(): try: moduleobject.matrix_stop(bot) except Exception: traceback.print_exc(file=sys.stderr) async def run(self): if not self.client.access_token: login_response = await self.client.login(self.matrix_pass) if isinstance(login_response, LoginError): self.logger.error(f"Failed to login: {login_response.message}") return last_16 = self.client.access_token[-16:] self.logger.info(f"Logged in with password, access token: ...{last_16}") await self.client.sync() for roomid, room in self.client.rooms.items(): self.logger.info(f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users") if len(room.users) == 1: self.logger.info(f'Room {roomid} has no other users - leaving it.') self.logger.info(await self.client.room_leave(roomid)) self.start() self.poll_task = asyncio.get_event_loop().create_task(self.poll_timer()) if self.client.logged_in: self.load_settings(self.get_account_data()) self.client.add_event_callback(self.message_cb, RoomMessageText) self.client.add_event_callback(self.invite_cb, (InviteEvent,)) if self.join_on_invite: self.logger.info('Note: Bot will join rooms if invited') self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners) self.bot_task = asyncio.create_task(self.client.sync_forever(timeout=30000)) await self.bot_task else: self.logger.error('Client was not able to log in, check env variables!') async def shutdown(self): if self.client.logged_in: logout = await self.client.logout() if isinstance(logout, LogoutResponse): self.logger.info("Logout successful") try: await self.client.close() self.logger.info("Connection closed") except Exception as e: self.logger.error("error while closing client: %s", e) else: logout: LogoutError self.logger.error(f"Logout unsuccessful. msg: {logout.message}") else: await self.client.client_session.close()
async def main(): # Read config file # A different config file path can be specified as the first command line arg if len(sys.argv) > 1: config_filepath = sys.argv[1] else: config_filepath = "config.yaml" CONFIG.read_config(config_filepath) # Configure the python job scheduler SCHEDULER.configure({"apscheduler.timezone": CONFIG.timezone}) # 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, ) # Configure the database store = Storage(client) # Set up event callbacks callbacks = Callbacks(client, store) client.add_event_callback(callbacks.message, (RoomMessageText,)) client.add_event_callback(callbacks.invite, (InviteMemberEvent,)) client.add_event_callback(callbacks.decryption_failure, (MegolmEvent,)) # Keep trying to reconnect on failure (with some time in-between) 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. Usually incorrect password if type(login_response) == LoginError: logger.error("Failed to login: %s", login_response.message) logger.warning("Trying again in 15s...") # Sleep so we don't bombard the server with login requests sleep(15) continue 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}") logger.info("Startup complete") # Allow jobs to fire try: SCHEDULER.start() except SchedulerAlreadyRunningError: pass await client.sync_forever(timeout=30000, full_state=True) except (ClientConnectionError, ServerDisconnectedError, TimeoutError): logger.warning("Unable to connect to homeserver, retrying in 15s...") # Sleep so we don't bombard the server with login requests sleep(15) except Exception: logger.exception("Unknown exception occurred:") logger.warning("Restarting 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 MatrixBot: def __init__(self, config): self.config = config self.modules = [] if self.config['main'].get('debug', '').lower() == 'true': self.debug = True else: self.debug = False for key, value in modules.__dict__.items(): if inspect.isclass(value) and issubclass(value, MatrixBotModule): print("loading {}...".format(value)) m = value.create(config) if m: self.modules.append(m) async def send_room_text(self, room, content): await self.client.room_send(room_id=room.room_id, message_type="m.room.message", content={ "body": content, "msgtype": "m.text" }) async def send_room_html(self, room, content): await self.client.room_send(room_id=room.room_id, message_type="m.room.message", content={ "body": re.sub('<[^<]+?>', '', content), "msgtype": "m.text", "format": "org.matrix.custom.html", "formatted_body": content, }) async def send_room_content(self, room, msgtype, url, name, extra): await self.client.room_send(room_id=room.room_id, message_type="m.room.message", content={ "body": name, "msgtype": msgtype, "url": url, "info": extra, }) async def send_room_image(self, room, url, name, extra): await self.send_room_content(room=room, msgtype="m.image", url=url, name=name, extra=extra) async def send_room_file(self, room, url, name, extra): await self.send_room_content(room=room, msgtype="m.file", url=url, name=name, extra=extra) async def on_room_message(self, room, event): if event.sender == self.client.user: return for module in self.modules: try: await module.handle_room_message(self, room, event) except Exception as e: if self.debug: msg = E.PRE(traceback.format_exc()) html_data = lxml.html.tostring(msg).decode('utf-8') await self.send_room_html(room=room, content=html_data) else: await self.send_room_text(room=room, content="There was an error.") async def on_invite(self, room, event): self.client.join(room.room_id) async def run(self): client_config = ClientConfig(store_sync_tokens=True) self.client = AsyncClient(homeserver=self.config["main"]["base_url"], user=self.config["main"]["user_id"], device_id=self.config["main"]["device_id"], store_path=self.config["main"]["store_path"], config=client_config) self.client.add_event_callback(self.on_invite, InviteEvent) self.client.add_event_callback(self.on_room_message, RoomMessageText) print("Logging in...") status = await self.client.login( self.config["main"]["password"], device_name=self.config["main"]["device_name"]) if not self.client.logged_in: print("Error logging in.") return print(f"Logged in as user {self.client.user_id}") await self.client.sync_forever(timeout=30000, full_state=True)
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 MatrixBot: def __init__(self): self.client = None async def on_message(self, room: MatrixRoom, event: RoomMessageText) -> None: if event.sender == self.client.user_id: return print( f"{strftime('%Y-%m-%d %H:%M:%S', localtime(event.server_timestamp/1000))}: {room.room_id}({room.display_name})|<{event.sender}({room.user_name(event.sender)})> {event.body}" ) if time() - event.server_timestamp / 1000 > config.max_timediff: print(" ` skip old message") return msg = event.body msgtr = common.process_message( msg, config.default_tabs, config.min_levenshtein_ratio, "[TEST MODE] " if config.test_mode else False) if msgtr: content = { "msgtype": "m.text", "body": msgtr, } m_relates_to = event.source["content"].get("m.relates_to", None) if m_relates_to and m_relates_to.get("rel_type", None) == "io.element.thread": content["m.relates_to"] = { "rel_type": "io.element.thread", "event_id": m_relates_to.get("event_id", None), } await self.client.room_send( room_id=room.room_id, message_type="m.room.message", content=content, ignore_unverified_devices=True, ) async def on_invite(self, room: MatrixRoom, event: InviteMemberEvent) -> None: print( f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}: <<< Invited to {room.room_id} by {event.sender} >>>" ) await self.client.join(room.room_id) async def run(self) -> None: client_config = AsyncClientConfig(store_sync_tokens=True, encryption_enabled=True) self.client = AsyncClient(config.matrix_homeserver, store_path="./store", config=client_config) self.client.restore_login( user_id=config.matrix_user_id, device_id=config.matrix_device_id, access_token=config.matrix_access_token, ) if self.client.should_upload_keys: await self.client.keys_upload() if self.client.should_query_keys: await self.client.keys_query() if self.client.should_claim_keys: await self.client.keys_claim() await self.client.sync(full_state=True) self.client.add_event_callback(self.on_message, RoomMessageText) self.client.add_event_callback(self.on_invite, InviteMemberEvent) await self.client.sync_forever(timeout=30000) await self.client.close()
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)
async def main(): """Entry point.""" # 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, ) # Assign an access token to the bot instead of logging in and creating a new device client.access_token = config.access_token # 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_event_callback(callbacks.joined, (RoomMemberEvent,)) # Create a new sync token, attempting to load one from the database if it has one already sync_token = SyncToken(store) # Keep trying to reconnect on failure (with some time in-between) 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: %s", login_response.message) # return False # 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("Error logging in: %s", e) # return False # 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) # =============================== # =============================== logger.debug("Syncing: %s", sync_token.token) sync_response = await client.sync(timeout=30000, since=sync_token.token) # Check if the sync had an errors if type(sync_response) == SyncError: logger.warning("Error in client sync: %s", sync_response.message) continue # Save the latest sync token to the database token = sync_response.next_batch if token: sync_token.update(token)
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)
async def main(): # Read 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" 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) # noinspection PyTypeChecker client.add_event_callback(callbacks.message, (RoomMessageText, )) # noinspection PyTypeChecker client.add_event_callback(callbacks.invite, (InviteMemberEvent, )) # 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) break 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, ) break # Login succeeded! # Join the management room or fail response = await with_ratelimit(client, "join", config.management_room) if type(response) == JoinError: logger.fatal("Could not join the management room, aborting.") break else: logger.info(f"Management room membership is good") # Resolve management room ID if not known if config.management_room.startswith('#'): # Resolve the room ID response = await with_ratelimit(client, "room_resolve_alias", config.management_room) if type(response) == RoomResolveAliasResponse: config.management_room_id = response.room_id else: logger.fatal( "Could not resolve the management room ID from alias, aborting" ) break logger.info(f"Logged in as {config.user_id}") 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): self.appid = 'org.vranki.hemppa' self.version = '1.4' self.client = None self.join_on_invite = False self.modules = dict() self.pollcount = 0 self.poll_task = None self.owners = [] self.debug = os.getenv("DEBUG", "false").lower() == "true" self.logger = None self.jointime = None # HACKHACKHACK to avoid running old commands after join self.join_hack_time = 5 # Seconds self.initialize_logger() def initialize_logger(self): if os.path.exists('config/logging.yml'): with open('config/logging.yml') as f: config = yaml.load(f, Loader=yaml.Loader) logging.config.dictConfig(config) else: log_format = '%(levelname)s - %(name)s - %(message)s' logging.basicConfig(format=log_format) self.logger = logging.getLogger("hemppa") if self.debug: logging.root.setLevel(logging.DEBUG) self.logger.info("enabled debugging") self.logger.debug("Logger initialized") async def send_text(self, room, body, msgtype="m.notice", bot_ignore=False): msg = { "body": body, "msgtype": msgtype, } if bot_ignore: msg["org.vranki.hemppa.ignore"] = "true" await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_html(self, room, html, plaintext, msgtype="m.notice", bot_ignore=False): msg = { "msgtype": msgtype, "format": "org.matrix.custom.html", "formatted_body": html, "body": plaintext } if bot_ignore: msg["org.vranki.hemppa.ignore"] = "true" await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_image(self, room, url, body): """ :param room: A MatrixRoom the image should be send to :param url: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri :param body: A textual representation of the image :return: """ msg = {"url": url, "body": body, "msgtype": "m.image"} await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_msg(self, mxid, roomname, message): # Sends private message to user. Returns true on success. # Find if we already have a common room with user: msg_room = None for croomid in self.client.rooms: roomobj = self.client.rooms[croomid] if len(roomobj.users) == 2: for user in roomobj.users: if user == mxid: msg_room = roomobj # Nope, let's create one if not msg_room: msg_room = await self.client.room_create( visibility=RoomVisibility.private, name=roomname, is_direct=True, preset=RoomPreset.private_chat, invite={mxid}, ) if not msg_room or (type(msg_room) is RoomCreateError): self.logger.error( f'Unable to create room when trying to message {mxid}') return False # Send message to the room await self.send_text(msg_room, message) return True def remove_callback(self, callback): for cb_object in self.client.event_callbacks: if cb_object.func == callback: self.logger.info("remove callback") self.client.event_callbacks.remove(cb_object) def get_room_by_id(self, room_id): return self.client.rooms[room_id] # Throws exception if event sender is not a room admin def must_be_admin(self, room, event): if not self.is_admin(room, event): raise CommandRequiresAdmin # Throws exception if event sender is not a bot owner def must_be_owner(self, event): if not self.is_owner(event): raise CommandRequiresOwner # Returns true if event's sender is admin in the room event was sent in, # or is bot owner def is_admin(self, room, event): if self.is_owner(event): return True if event.sender not in room.power_levels.users: return False return room.power_levels.users[event.sender] >= 50 # Returns true if event's sender is owner of the bot def is_owner(self, event): return event.sender in self.owners # Checks if this event should be ignored by bot, including custom property def should_ignore_event(self, event): return "org.vranki.hemppa.ignore" in event.source['content'] def save_settings(self): module_settings = dict() for modulename, moduleobject in self.modules.items(): try: module_settings[modulename] = moduleobject.get_settings() except Exception: traceback.print_exc(file=sys.stderr) data = {self.appid: self.version, 'module_settings': module_settings} self.set_account_data(data) def load_settings(self, data): if not data: return if not data.get('module_settings'): return for modulename, moduleobject in self.modules.items(): if data['module_settings'].get(modulename): try: moduleobject.set_settings( data['module_settings'][modulename]) except Exception: traceback.print_exc(file=sys.stderr) async def message_cb(self, room, event): # Ignore if asked to ignore if self.should_ignore_event(event): print('Ignoring this!') return body = event.body # Figure out the command if not self.starts_with_command(body): return if self.owners_only and not self.is_owner(event): self.logger.info( f"Ignoring {event.sender}, because they're not an owner") await self.send_text(room, "Sorry, only bot owner can run commands.") return # HACK to ignore messages for some time after joining. if self.jointime: if (datetime.datetime.now() - self.jointime).seconds < self.join_hack_time: self.logger.info( f"Waiting for join delay, ignoring message: {body}") return self.jointime = None command = body.split().pop(0) # Strip away non-alphanumeric characters, including leading ! for security command = re.sub(r'\W+', '', command) moduleobject = self.modules.get(command) if moduleobject is not None: if moduleobject.enabled: try: await moduleobject.matrix_message(self, room, event) except CommandRequiresAdmin: await self.send_text( room, f'Sorry, you need admin power level in this room to run that command.' ) except CommandRequiresOwner: await self.send_text( room, f'Sorry, only bot owner can run that command.') except Exception: await self.send_text( room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details' ) traceback.print_exc(file=sys.stderr) else: self.logger.error(f"Unknown command: {command}") # TODO Make this configurable # await self.send_text(room, # f"Sorry. I don't know what to do. Execute !help to get a list of available commands.") @staticmethod def starts_with_command(body): """Checks if body starts with ! and has one or more letters after it""" return re.match(r"^!\w.*", body) is not None async def invite_cb(self, room, event): room: MatrixRoom event: InviteEvent if self.join_on_invite or self.is_owner(event): for attempt in range(3): self.jointime = datetime.datetime.now() result = await self.client.join(room.room_id) if type(result) == JoinError: self.logger.error( f"Error joining room %s (attempt %d): %s", room.room_id, attempt, result.message) else: self.logger.info( f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'" ) return else: self.logger.warning( f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}' ) async def memberevent_cb(self, room, event): # Automatically leaves rooms where bot is alone. if room.member_count == 1 and event.membership == 'leave': self.logger.info( f"membership event in {room.display_name} ({room.room_id}) with {room.member_count} members by '{event.sender}' - leaving room as i don't want to be left alone!" ) await self.client.room_leave(room.room_id) def load_module(self, modulename): try: self.logger.info(f'load module: {modulename}') module = importlib.import_module('modules.' + modulename) module = reload(module) cls = getattr(module, 'MatrixModule') return cls(modulename) except ModuleNotFoundError: self.logger.error(f'Module {modulename} failed to load!') traceback.print_exc(file=sys.stderr) return None def reload_modules(self): for modulename in self.modules: self.logger.info(f'Reloading {modulename} ..') self.modules[modulename] = self.load_module(modulename) self.load_settings(self.get_account_data()) def get_modules(self): modulefiles = glob.glob('./modules/*.py') for modulefile in modulefiles: modulename = os.path.splitext(os.path.basename(modulefile))[0] moduleobject = self.load_module(modulename) if moduleobject: self.modules[modulename] = moduleobject def clear_modules(self): self.modules = dict() async def poll_timer(self): while True: self.pollcount = self.pollcount + 1 for modulename, moduleobject in self.modules.items(): if moduleobject.enabled: try: await moduleobject.matrix_poll(self, self.pollcount) except Exception: traceback.print_exc(file=sys.stderr) await asyncio.sleep(10) def set_account_data(self, data): userid = urllib.parse.quote(self.matrix_user) ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}" response = requests.put(ad_url, json.dumps(data)) self.__handle_error_response(response) if response.status_code != 200: self.logger.error( 'Setting account data failed. response: %s json: %s', response, response.json()) def get_account_data(self): userid = urllib.parse.quote(self.matrix_user) ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}" response = requests.get(ad_url) self.__handle_error_response(response) if response.status_code == 200: return response.json() self.logger.error( f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.' ) return None def __handle_error_response(self, response): if response.status_code == 401: self.logger.error("access token is invalid or missing") self.logger.info("NOTE: check MATRIX_ACCESS_TOKEN") sys.exit(2) def init(self): self.matrix_user = os.getenv('MATRIX_USER') matrix_server = os.getenv('MATRIX_SERVER') bot_owners = os.getenv('BOT_OWNERS') access_token = os.getenv('MATRIX_ACCESS_TOKEN') join_on_invite = os.getenv('JOIN_ON_INVITE') owners_only = os.getenv('OWNERS_ONLY') is not None if matrix_server and self.matrix_user and bot_owners and access_token: self.client = AsyncClient(matrix_server, self.matrix_user) self.client.access_token = access_token self.join_on_invite = join_on_invite is not None self.owners = bot_owners.split(',') self.owners_only = owners_only self.get_modules() else: self.logger.error( "The environment variables MATRIX_SERVER, MATRIX_USER, MATRIX_ACCESS_TOKEN and BOT_OWNERS are mandatory" ) sys.exit(1) def start(self): self.load_settings(self.get_account_data()) enabled_modules = [ module for module_name, module in self.modules.items() if module.enabled ] self.logger.info(f'Starting {len(enabled_modules)} modules..') for modulename, moduleobject in self.modules.items(): if moduleobject.enabled: try: moduleobject.matrix_start(self) except Exception: traceback.print_exc(file=sys.stderr) def stop(self): self.logger.info(f'Stopping {len(self.modules)} modules..') for modulename, moduleobject in self.modules.items(): try: moduleobject.matrix_stop(self) except Exception: traceback.print_exc(file=sys.stderr) async def run(self): await self.client.sync() for roomid, room in self.client.rooms.items(): self.logger.info( f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users" ) if len(room.users) == 1: self.logger.info( f'Room {roomid} has no other users - leaving it.') self.logger.info(await self.client.room_leave(roomid)) self.start() self.poll_task = asyncio.get_event_loop().create_task( self.poll_timer()) if self.client.logged_in: self.load_settings(self.get_account_data()) self.client.add_event_callback(self.message_cb, RoomMessageText) self.client.add_event_callback(self.invite_cb, (InviteEvent, )) self.client.add_event_callback(self.memberevent_cb, (RoomMemberEvent, )) if self.join_on_invite: self.logger.info('Note: Bot will join rooms if invited') self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners) self.bot_task = asyncio.create_task( self.client.sync_forever(timeout=30000)) await self.bot_task else: self.logger.error( 'Client was not able to log in, check env variables!') async def shutdown(self): await self.close() async def close(self): try: await self.client.close() self.logger.info("Connection closed") except Exception as ex: self.logger.error("error while closing client: %s", ex) def handle_exit(self, signame, loop): self.logger.info(f"Received signal {signame}") if self.poll_task: self.poll_task.cancel() self.bot_task.cancel() self.stop()
class Bot(): def __init__(self, controller, creds): self.name = controller.MATRIXBZ_BOT_NAME self.CHANNEL_GREETING = controller.CHANNEL_GREETING self.user = creds['user'] self.password = creds['password'] self.client = AsyncClient(creds['homeserver'], creds['user']) 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.commands = {} self.msg_handler = None self.startup_method = None members = inspect.getmembers(controller, predicate=inspect.ismethod) for member in members: if hasattr(member[1], 'matrixbz_method'): # add member[1] command_str = f'!{self.name} {member[0]}' self.commands[command_str] = member[1] elif hasattr(member[1], 'matrixbz_msg_handler'): if self.msg_handler: raise Exception('Can only mark one matrixbz_msg_handler!') self.msg_handler = member[1] elif hasattr(member[1], 'matrixbz_startup_method'): if self.startup_method: raise Exception( 'Can only mark one matrixbz_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 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 _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())
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(): """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.message, (RoomMessageText, )) client.add_event_callback(callbacks.invite, (InviteMemberEvent, )) client.add_event_callback(callbacks.decryption_failure, (MegolmEvent, )) client.add_event_callback(callbacks.unknown, (UnknownEvent, )) # Set up a scheduler scheduler = AsyncIOScheduler() # Set up MISPAlert misp_alert = MISPAlert(client, config, store) # Add a job that checks for new taged events every minute trigger = IntervalTrigger( seconds=60, start_date=datetime.now() + timedelta(seconds=2), ) # Add the query job scheduler.add_job(misp_alert.alerter, trigger=trigger) # 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}") # Allow jobs to fire try: scheduler.start() except SchedulerAlreadyRunningError: pass 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()
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(): # 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, )) # Keep trying to reconnect on failure (with some time in-between) 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: %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! # 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): 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): self.appid = 'org.vranki.hemppa' self.version = '1.5' self.client = None self.join_on_invite = False self.modules = dict() self.module_aliases = dict() self.leave_empty_rooms = True self.uri_cache = dict() self.pollcount = 0 self.poll_task = None self.owners = [] self.debug = os.getenv("DEBUG", "false").lower() == "true" self.logger = None self.jointime = None # HACKHACKHACK to avoid running old commands after join self.join_hack_time = 5 # Seconds self.initialize_logger() def initialize_logger(self): if os.path.exists('config/logging.yml'): with open('config/logging.yml') as f: config = yaml.load(f, Loader=yaml.Loader) logging.config.dictConfig(config) else: log_format = '%(levelname)s - %(name)s - %(message)s' logging.basicConfig(format=log_format) self.logger = logging.getLogger("hemppa") if self.debug: logging.root.setLevel(logging.DEBUG) self.logger.info("enabled debugging") self.logger.debug("Logger initialized") async def upload_and_send_image(self, room, url, text=None, blob=False, blob_content_type="image/png"): """ :param room: A MatrixRoom the image should be send to after uploading :param url: Url of binary content of the image to upload :param text: A textual representation of the image :param blob: Flag to indicate if the second param is an url or a binary content :param blob_content_type: Content type of the image in case of binary content :return: """ cache_key = url if blob: ## url is bytes, cannot be used a key for cache cache_key = hashlib.md5(url).hexdigest() try: matrix_uri, mimetype, w, h, size = self.uri_cache[cache_key] except KeyError: try: res = await self.upload_image(url, blob, blob_content_type) matrix_uri, mimetype, w, h, size = res self.uri_cache[cache_key] = list(res) self.save_settings() except UploadFailed: return await self.send_text(room, f"Sorry. Something went wrong fetching {url} and uploading it to the image to matrix server :(") if not text and not blob: text = f"{url}" return await self.send_image(room, matrix_uri, text, mimetype, w, h, size) # Helper function to upload a image from URL to homeserver. Use send_image() to actually send it to room. # Throws exception if upload fails async def upload_image(self, url_or_bytes, blob=False, blob_content_type="image/png"): """ :param url_or_bytes: Url or binary content of the image to upload :param blob: Flag to indicate if the first param is an url or a binary content :param blob_content_type: Content type of the image in case of binary content :return: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri, Content type, Width, Height, Image size in bytes """ self.client: AsyncClient response: UploadResponse if blob: (response, alist) = await self.client.upload(lambda a, b: url_or_bytes, blob_content_type) i = Image.open(BytesIO(url_or_bytes)) image_length = len(url_or_bytes) content_type = blob_content_type else: self.logger.debug(f"start downloading image from url {url_or_bytes}") headers = {'User-Agent': 'Mozilla/5.0'} url_response = requests.get(url_or_bytes, headers=headers) self.logger.debug(f"response [status_code={url_response.status_code}, headers={url_response.headers}") if url_response.status_code == 200: content_type = url_response.headers.get("content-type") self.logger.info(f"uploading content to matrix server [size={len(url_response.content)}, content-type: {content_type}]") (response, alist) = await self.client.upload(lambda a, b: url_response.content, content_type) self.logger.debug("response: %s", response) i = Image.open(BytesIO(url_response.content)) image_length = len(url_response.content) else: self.logger.error("unable to request url: %s", url_response) raise UploadFailed if isinstance(response, UploadResponse): self.logger.info("uploaded file to %s", response.content_uri) return response.content_uri, content_type, i.size[0], i.size[1], image_length else: response: UploadError self.logger.error("unable to upload file. msg: %s", response.message) raise UploadFailed async def send_text(self, room, body, msgtype="m.notice", bot_ignore=False): """ :param room: A MatrixRoom the text should be send to :param body: Textual content of the message :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes :param bot_ignore: Flag to mark the message to be ignored by the bot :return: the NIO Response from room_send() """ msg = { "body": body, "msgtype": msgtype, } if bot_ignore: msg["org.vranki.hemppa.ignore"] = "true" return await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_html(self, room, html, plaintext, msgtype="m.notice", bot_ignore=False): """ :param room: A MatrixRoom the html should be send to :param html: Html content of the message :param plaintext: Plaintext content of the message :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes :param bot_ignore: Flag to mark the message to be ignored by the bot :return: """ msg = { "msgtype": msgtype, "format": "org.matrix.custom.html", "formatted_body": html, "body": plaintext } if bot_ignore: msg["org.vranki.hemppa.ignore"] = "true" await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_location(self, room, body, latitude, longitude, bot_ignore=False): """ :param room: A MatrixRoom the html should be send to :param html: Html content of the message :param body: Plaintext content of the message :param latitude: Latitude in WGS84 coordinates (float) :param longitude: Longitude in WGS84 coordinates (float) :param bot_ignore: Flag to mark the message to be ignored by the bot :return: """ locationmsg = { "body": str(body), "geo_uri": 'geo:' + str(latitude) + ',' + str(longitude), "msgtype": "m.location", } await self.client.room_send(room.room_id, 'm.room.message', locationmsg) async def send_image(self, room, url, body, mimetype=None, width=None, height=None, size=None): """ :param room: A MatrixRoom the image should be send to :param url: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri :param body: A textual representation of the image :param mimetype: The mimetype of the image :param width: Width in pixel of the image :param height: Height in pixel of the image :param size: Size in bytes of the image :return: """ msg = { "url": url, "body": body, "msgtype": "m.image", "info": { "thumbnail_info": None, "thumbnail_url": None, }, } if mimetype: msg["info"]["mimetype"] = mimetype if width: msg["info"]["w"] = width if height: msg["info"]["h"] = height if size: msg["info"]["size"] = size return await self.client.room_send(room.room_id, 'm.room.message', msg) async def send_msg(self, mxid, roomname, message): """ :param mxid: A Matrix user id to send the message to :param roomname: A Matrix room id to send the message to :param message: Text to be sent as message :return bool: Success upon sending the message """ # Sends private message to user. Returns true on success. msg_room = await self.find_or_create_private_msg(mxid, roomname) if not msg_room or (type(msg_room) is RoomCreateError): self.logger.error(f'Unable to create room when trying to message {mxid}') return False # Send message to the room await self.send_text(msg_room, message) return True async def find_or_create_private_msg(self, mxid, roomname): # Find if we already have a common room with user: msg_room = None for croomid in self.client.rooms: roomobj = self.client.rooms[croomid] if len(roomobj.users) == 2: for user in roomobj.users: if user == mxid: msg_room = roomobj # Nope, let's create one if not msg_room: msg_room = await self.client.room_create(visibility=RoomVisibility.private, name=roomname, is_direct=True, preset=RoomPreset.private_chat, invite={mxid}, ) return msg_room def remove_callback(self, callback): for cb_object in self.client.event_callbacks: if cb_object.func == callback: self.logger.info("remove callback") self.client.event_callbacks.remove(cb_object) def get_room_by_id(self, room_id): try: return self.client.rooms[room_id] except KeyError: return None async def get_room_by_alias(self, alias): rar = await self.client.room_resolve_alias(alias) if type(rar) is RoomResolveAliasResponse: return rar.room_id return None # Throws exception if event sender is not a room admin def must_be_admin(self, room, event, power_level=50): if not self.is_admin(room, event, power_level=power_level): raise CommandRequiresAdmin # Throws exception if event sender is not a bot owner def must_be_owner(self, event): if not self.is_owner(event): raise CommandRequiresOwner # Returns true if event's sender has PL50 or more in the room event was sent in, # or is bot owner def is_admin(self, room, event, power_level=50): if self.is_owner(event): return True if event.sender not in room.power_levels.users: return False return room.power_levels.users[event.sender] >= power_level # Returns true if event's sender is owner of the bot def is_owner(self, event): return event.sender in self.owners # Checks if this event should be ignored by bot, including custom property def should_ignore_event(self, event): return "org.vranki.hemppa.ignore" in event.source['content'] def save_settings(self): module_settings = dict() for modulename, moduleobject in self.modules.items(): try: module_settings[modulename] = moduleobject.get_settings() except Exception: self.logger.exception(f'unhandled exception {modulename}.get_settings') data = {self.appid: self.version, 'module_settings': module_settings, 'uri_cache': self.uri_cache} self.set_account_data(data) def load_settings(self, data): if not data: return if not data.get('module_settings'): return if data.get('uri_cache'): self.uri_cache = data['uri_cache'] for modulename, moduleobject in self.modules.items(): if data['module_settings'].get(modulename): try: moduleobject.set_settings( data['module_settings'][modulename]) except Exception: self.logger.exception(f'unhandled exception {modulename}.set_settings') async def message_cb(self, room, event): # Ignore if asked to ignore if self.should_ignore_event(event): if self.debug: self.logger.debug('Ignoring event!') return body = event.body # Figure out the command if not self.starts_with_command(body): return if self.owners_only and not self.is_owner(event): self.logger.info(f"Ignoring {event.sender}, because they're not an owner") await self.send_text(room, "Sorry, only bot owner can run commands.") return # HACK to ignore messages for some time after joining. if self.jointime: if (datetime.datetime.now() - self.jointime).seconds < self.join_hack_time: self.logger.info(f"Waiting for join delay, ignoring message: {body}") return self.jointime = None command = body.split().pop(0) # Strip away non-alphanumeric characters, including leading ! for security command = re.sub(r'\W+', '', command) # Fallback to any declared aliases moduleobject = self.modules.get(command) or self.modules.get(self.module_aliases.get(command)) if moduleobject is not None: if moduleobject.enabled: try: await moduleobject.matrix_message(self, room, event) except CommandRequiresAdmin: await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.') except CommandRequiresOwner: await self.send_text(room, f'Sorry, only bot owner can run that command.') except Exception: await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details') self.logger.exception(f'unhandled exception in !{command}') else: self.logger.error(f"Unknown command: {command}") # TODO Make this configurable # await self.send_text(room, # f"Sorry. I don't know what to do. Execute !help to get a list of available commands.") @staticmethod def starts_with_command(body): """Checks if body starts with ! and has one or more letters after it""" return re.match(r"^!\w.*", body) is not None async def invite_cb(self, room, event): room: MatrixRoom event: InviteEvent if self.join_on_invite or self.is_owner(event): for attempt in range(3): self.jointime = datetime.datetime.now() result = await self.client.join(room.room_id) if type(result) == JoinError: self.logger.error(f"Error joining room %s (attempt %d): %s", room.room_id, attempt, result.message) else: self.logger.info(f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'") return else: self.logger.warning(f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}') async def memberevent_cb(self, room, event): # Automatically leaves rooms where bot is alone. if room.member_count == 1 and event.membership=='leave' and event.sender != self.matrix_user: self.logger.info(f"Membership event in {room.display_name} ({room.room_id}) with {room.member_count} members by '{event.sender}' (I am {self.matrix_user})- leaving room as i don't want to be left alone!") await self.client.room_leave(room.room_id) def load_module(self, modulename): try: self.logger.info(f'load module: {modulename}') module = importlib.import_module('modules.' + modulename) module = reload(module) cls = getattr(module, 'MatrixModule') return cls(modulename) except Exception: self.logger.exception(f'Module {modulename} failed to load') return None def reload_modules(self): for modulename in self.modules: self.logger.info(f'Reloading {modulename} ..') self.modules[modulename] = self.load_module(modulename) self.load_settings(self.get_account_data()) def get_modules(self): modulefiles = glob.glob('./modules/*.py') for modulefile in modulefiles: modulename = os.path.splitext(os.path.basename(modulefile))[0] moduleobject = self.load_module(modulename) if moduleobject: self.modules[modulename] = moduleobject def clear_modules(self): self.modules = dict() async def poll_timer(self): while True: self.pollcount = self.pollcount + 1 for modulename, moduleobject in self.modules.items(): if moduleobject.enabled: try: await moduleobject.matrix_poll(self, self.pollcount) except Exception: self.logger.exception(f'unhandled exception from {modulename}.matrix_poll') await asyncio.sleep(10) def set_account_data(self, data): userid = urllib.parse.quote(self.matrix_user) ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}" response = requests.put(ad_url, json.dumps(data)) self.__handle_error_response(response) if response.status_code != 200: self.logger.error('Setting account data failed. response: %s json: %s', response, response.json()) def get_account_data(self): userid = urllib.parse.quote(self.matrix_user) ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}" response = requests.get(ad_url) self.__handle_error_response(response) if response.status_code == 200: return response.json() self.logger.error(f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.') return None def __handle_error_response(self, response): if response.status_code == 401: self.logger.error("access token is invalid or missing") self.logger.info("NOTE: check MATRIX_ACCESS_TOKEN") sys.exit(2) def init(self): self.matrix_user = os.getenv('MATRIX_USER') matrix_server = os.getenv('MATRIX_SERVER') bot_owners = os.getenv('BOT_OWNERS') access_token = os.getenv('MATRIX_ACCESS_TOKEN') join_on_invite = os.getenv('JOIN_ON_INVITE') owners_only = os.getenv('OWNERS_ONLY') is not None leave_empty_rooms = os.getenv('LEAVE_EMPTY_ROOMS') if matrix_server and self.matrix_user and bot_owners and access_token: self.client = AsyncClient(matrix_server, self.matrix_user, ssl = matrix_server.startswith("https://")) self.client.access_token = access_token self.join_on_invite = (join_on_invite or '').lower() == 'true' self.leave_empty_rooms = (leave_empty_rooms or 'true').lower() == 'true' self.owners = bot_owners.split(',') self.owners_only = owners_only self.get_modules() else: self.logger.error("The environment variables MATRIX_SERVER, MATRIX_USER, MATRIX_ACCESS_TOKEN and BOT_OWNERS are mandatory") sys.exit(1) def start(self): self.load_settings(self.get_account_data()) enabled_modules = [module for module_name, module in self.modules.items() if module.enabled] self.logger.info(f'Starting {len(enabled_modules)} modules..') for modulename, moduleobject in self.modules.items(): if moduleobject.enabled: try: moduleobject.matrix_start(self) except Exception: self.logger.exception(f'unhandled exception from {modulename}.matrix_start') self.logger.info(f'All modules started.') def stop(self): self.logger.info(f'Stopping {len(self.modules)} modules..') for modulename, moduleobject in self.modules.items(): try: moduleobject.matrix_stop(self) except Exception: self.logger.exception(f'unhandled exception from {modulename}.matrix_stop') self.logger.info(f'All modules stopped.') async def run(self): await self.client.sync() for roomid, room in self.client.rooms.items(): self.logger.info(f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users") if len(room.users) == 1 and self.leave_empty_rooms: self.logger.info(f'Room {roomid} has no other users - leaving it.') self.logger.info(await self.client.room_leave(roomid)) self.start() self.poll_task = asyncio.get_event_loop().create_task(self.poll_timer()) if self.client.logged_in: self.load_settings(self.get_account_data()) self.client.add_event_callback(self.message_cb, RoomMessageText) self.client.add_event_callback(self.invite_cb, (InviteEvent,)) self.client.add_event_callback(self.memberevent_cb, (RoomMemberEvent,)) if self.join_on_invite: self.logger.info('Note: Bot will join rooms if invited') self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners) self.bot_task = asyncio.create_task(self.client.sync_forever(timeout=30000)) await self.bot_task else: self.logger.error('Client was not able to log in, check env variables!') async def shutdown(self): await self.close() async def close(self): try: await self.client.close() self.logger.info("Connection closed") except Exception as ex: self.logger.error("error while closing client: %s", ex) def handle_exit(self, signame, loop): self.logger.info(f"Received signal {signame}") if self.poll_task: self.poll_task.cancel() self.bot_task.cancel() self.stop()
class MatrixInterface(MessengerInterface): display_name: str avatar_path: str identifier: str username: str matrix: AsyncClient bot: Bot log = logging.getLogger(__name__) public_url: str web_dir: str debug: bool def __init__(self, bot: Bot, home_server: str, username: str, access_token: str, device_id: str, store_filepath: str, web_dir: str, public_url: str, display_name: str, avatar_path: str, debug: bool = False): if not os.path.exists(store_filepath): os.makedirs(store_filepath) self.debug = debug self.public_url = public_url self.web_dir = web_dir self.display_name = display_name self.avatar_path = avatar_path self.username = username self.identifier = f"@{username}:{home_server[home_server.find('//') + 2:]}" self.matrix = AsyncClient(home_server, self.identifier, device_id, store_path=store_filepath, config=AsyncClientConfig( encryption_enabled=True, store=SqliteStore, store_name="matrix.db", store_sync_tokens=True)) self.matrix.access_token = access_token self.matrix.user_id = self.identifier self.matrix.add_event_callback(self.handle_message, RoomMessageText) self.matrix.add_event_callback(self.crypto_event, MegolmEvent) self.matrix.add_event_callback(self.invite_event, InviteMemberEvent) self.matrix.add_event_callback(self.room_event, RoomMemberEvent) self.matrix.restore_login(self.identifier, device_id, access_token) self.bot = bot self.log.level = logging.DEBUG self.log.debug(f"Initialized Matrix Bot: {self.identifier}") async def crypto_event(self, room: MatrixRoom, event: MegolmEvent): self.log.error(f"Can't decrypt for {room.name}") if event.session_id not in self.matrix.outgoing_key_requests: self.log.warning(f"Fetching keys for {room.name}") resp = await self.matrix.request_room_key(event) if isinstance(resp, RoomKeyRequestResponse): self.log.info( f"Got Response for {resp.room_id}, start syncing") await self.matrix.sync(full_state=True) self.log.info("Finished sync") elif isinstance(resp, RoomKeyRequestError): self.log.error(f"Got Error for requesting room key: {resp}") async def invite_event(self, room: MatrixRoom, event: InviteMemberEvent): if not event.membership == "invite" or event.state_key != self.matrix.user_id: return self.log.debug(f"Invite Event for {room.name}") resp = await self.matrix.join(room.room_id) if isinstance(resp, JoinError): self.log.error( f"Can't Join {room.room_id} ({room.encrypted}): {JoinError.message}" ) return await self.matrix.sync() self.log.debug(f"Joined room {room.name}") await self.send_response(room.room_id, self.bot.handle_input('Start', room.room_id)) if room.member_count > 2: await self.send_response(room.room_id, [ BotResponse( "Noch ein Hinweis: Da wir hier nicht zu zweit sind reagiere ich nur auf mentions!" ) ]) async def room_event(self, room: MatrixRoom, event: RoomMemberEvent): self.log.debug(f"Got RoomEvent: {event}") if event.membership == "leave" and event.state_key != self.matrix.user_id: if room.member_count == 1: resp = await self.matrix.room_leave(room.room_id) self.log.debug(f"Left room: {resp}") if isinstance(resp, RoomLeaveResponse): self.bot.delete_user(room.room_id) elif event.membership == "leave" and event.state_key == self.matrix.user_id: self.log.info( f"Got kicked from {room.name}: {event.content['reason']}") @prometheus_async.aio.time(BOT_RESPONSE_TIME) async def handle_message(self, room: MatrixRoom, event: RoomMessageText): if self.identifier == event.sender: self.log.debug("Skipped message from myself") return # We need a mention in group rooms to handle messages if event.body.startswith(self.display_name): event.body = event.body[len(self.display_name) + 1:].strip() else: if room.member_count > 2: self.log.debug( f"Skipped message in a group without mention: {event.body}" ) return RECV_MESSAGE_COUNT.inc() self.log.debug(f"Received from {room.room_id}: {event}") await self.send_response( room.room_id, self.bot.handle_input(event.body, room.room_id)) async def send_response(self, room_id: str, responses: List[BotResponse]): # Check if device is verified # if self.matrix.room_contains_unverified(room.room_id): # devices = self.matrix.room_devices(room.room_id) # for user in devices: # for device in devices[user]: # self.matrix.verify_device(devices[user][device]) # self.log.debug(f"Verified {device} of {user}") if self.debug: return for message in responses: if message.images: for image in message.images: # Calculate metadata mime_type = "image/jpeg" file_stat = os.stat(image) im = Image.open(image) (width, height) = im.size url = await self.upload_file(image, mime_type) image = { "body": os.path.basename(image), "msgtype": "m.image", "url": url, "info": { "size": file_stat.st_size, "mimetype": mime_type, "w": width, # width in pixel "h": height, # height in pixel }, } resp = await self.matrix.room_send( room_id=room_id, message_type="m.room.message", content=image, ignore_unverified_devices=True) if isinstance(resp, ErrorResponse): self.log.error(f"Could not send image: {resp}") else: SENT_IMAGES_COUNT.inc() resp = await self.matrix.room_send(room_id=room_id, message_type="m.room.message", content={ "msgtype": "m.text", "body": adapt_text(str(message), just_strip=True), "format": "org.matrix.custom.html", "formatted_body": str(message).replace( "\n", "<br />") }, ignore_unverified_devices=True) if isinstance(resp, ErrorResponse): self.log.error(f"Could not send message: {resp}") FAILED_MESSAGE_COUNT.inc() else: SENT_MESSAGE_COUNT.inc() async def upload_file(self, path: str, mime_type: str) -> Optional[str]: file_stat = os.stat(path) async with aiofiles.open(path, "r+b") as f: resp, maybe_keys = await self.matrix.upload( f, content_type=mime_type, filename=os.path.basename(path), filesize=file_stat.st_size) if not isinstance(resp, UploadResponse): self.log.error(f"Failed to upload file. Failure response: {resp}") return None return resp.content_uri async def async_run(self) -> None: # Needed to update all room members etc. self.log.debug("Start first full sync") await self.matrix.sync(full_state=True) self.log.debug("Finished first sync") self.log.debug("Check profile for completeness") profile = await self.matrix.get_profile(self.identifier) if profile.displayname != self.display_name: resp = await self.matrix.set_displayname(self.display_name) if isinstance(resp, ProfileSetDisplayNameError): self.log.error(f"Cant set display name: {resp}") else: self.log.debug(f"Set display name to {self.display_name}") if profile.avatar_url is None: url = await self.upload_file(self.avatar_path, "image/png") if url is not None: resp = await self.matrix.set_avatar(url) if isinstance(resp, ProfileSetAvatarError): self.log.error(f"Can't set avatar: {resp}") else: self.log.debug(f"Set avatar to {url}") await self.matrix.sync_forever(timeout=300) def run(self): asyncio.get_event_loop().run_until_complete(self.async_run()) async def send_unconfirmed_reports(self) -> None: unconfirmed_reports = self.bot.get_available_user_messages() if unconfirmed_reports: await self.matrix.sync(full_state=True) for report, userid, message in unconfirmed_reports: if not userid in self.matrix.rooms: self.log.error(f"Room {userid} does not exist") self.bot.disable_user(userid) continue try: await self.send_response(userid, message) except LocalProtocolError as e: self.log.warning( f"Error while sending report to {userid}: {e}") else: self.bot.confirm_message_send(report, userid) self.log.warning(f"Sent report to {userid}") await self.matrix.close() async def send_message_to_users(self, message: str, users: List[Union[str, int]]): for user in users: await self.send_response(user, [BotResponse(message)])
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()