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 login() -> AsyncClient: """Handle login with or without stored credentials.""" # Configuration options for the AsyncClient client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, encryption_enabled=True, ) # If there are no previously-saved credentials, we'll use the password if not os.path.exists(CONFIG_FILE): print("First time use. Did not find credential file. Asking for " "homeserver, user, and password to create credential file.") homeserver = "https://matrix.example.org" homeserver = input(f"Enter your homeserver URL: [{homeserver}] ") if not (homeserver.startswith("https://") or homeserver.startswith("http://")): homeserver = "https://" + homeserver user_id = "@user:example.org" user_id = input(f"Enter your full user ID: [{user_id}] ") device_name = "matrix-nio" device_name = input(f"Choose a name for this device: [{device_name}] ") if not os.path.exists(STORE_PATH): os.makedirs(STORE_PATH) # Initialize the matrix client client = AsyncClient( homeserver, user_id, store_path=STORE_PATH, config=client_config, ) pw = getpass.getpass() resp = await client.login(password=pw, device_name=device_name) # check that we logged in succesfully if (isinstance(resp, LoginResponse)): write_details_to_disk(resp, homeserver) else: print(f"homeserver = \"{homeserver}\"; user = \"{user_id}\"") print(f"Failed to log in: {resp}") sys.exit(1) print("Logged in using a password. Credentials were stored. " "On next execution the stored login credentials will be used.") # Otherwise the config file exists, so we'll use the stored credentials else: # open the file in read-only mode with open(CONFIG_FILE, "r") as f: config = json.load(f) # Initialize the matrix client based on credentials from file client = AsyncClient( config['homeserver'], config['user_id'], device_id=config['device_id'], store_path=STORE_PATH, config=client_config, ) client.restore_login(user_id=config['user_id'], device_id=config['device_id'], access_token=config['access_token']) print("Logged in using stored credentials.") return client
async def main(): # noqa """Create bot as Matrix client and enter event loop.""" # Read config file # A different config file path can be specified # as the first command line argument if len(sys.argv) > 1: config_filepath = sys.argv[1] else: config_filepath = "config.yaml" config = Config(config_filepath) # Configure the database store = Storage(config.database_filepath) # Configuration options for the AsyncClient client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, encryption_enabled=True, ) # Initialize the matrix client client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, store_path=config.store_filepath, config=client_config, ) # Set up event callbacks callbacks = Callbacks(client, store, config) client.add_event_callback(callbacks.message, (RoomMessageText, )) client.add_event_callback(callbacks.invite, (InviteMemberEvent, )) client.add_to_device_callback(callbacks.accept_all_verify, (KeyVerificationEvent, )) # Keep trying to reconnect on failure (with some time in-between) while True: try: # Try to login with the configured username/password try: if config.access_token: logger.debug("Using access token from config file to log " f"in. access_token={config.access_token}") client.restore_login(user_id=config.user_id, device_id=config.device_id, access_token=config.access_token) else: logger.debug("Using password from config file to log in.") login_response = await client.login( password=config.user_password, device_name=config.device_name, ) # Check if login failed if type(login_response) == LoginError: logger.error("Failed to login: "******"{login_response.message}") return False logger.info((f"access_token of device {config.device_name}" f" is: \"{login_response.access_token}\"")) except LocalProtocolError as e: # There's an edge case here where the user hasn't installed # the correct C dependencies. In that case, a # LocalProtocolError is raised on login. logger.fatal( "Failed to login. " "Have you installed the correct dependencies? " "Error: %s", e) return False # Login succeeded! logger.debug(f"Logged in successfully as user {config.user_id} " f"with device {config.device_id}.") # Sync encryption keys with the server # Required for participating in encrypted rooms if client.should_upload_keys: await client.keys_upload() if config.change_device_name: content = {"display_name": config.device_name} resp = await client.update_device(config.device_id, content) if isinstance(resp, UpdateDeviceError): logger.debug(f"update_device failed with {resp}") else: logger.debug(f"update_device successful with {resp}") await client.sync(timeout=30000, full_state=True) for device_id, olm_device in client.device_store[ config.user_id].items(): logger.info("Setting up trust for my own " f"device {device_id} and session key " f"{olm_device.keys}.") client.verify_device(olm_device) await client.sync_forever(timeout=30000, full_state=True) except (ClientConnectionError, ServerDisconnectedError): logger.warning( "Unable to connect to homeserver, retrying in 15s...") # Sleep so we don't bombard the server with login requests sleep(15) finally: # Make sure to close the client connection on disconnect await client.close()
class E2EEClient: def __init__(self, join_rooms: set): self.STORE_PATH = os.environ['LOGIN_STORE_PATH'] self.CONFIG_FILE = f"{self.STORE_PATH}/credentials.json" self.join_rooms = join_rooms self.client: AsyncClient = None self.client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, encryption_enabled=True, ) self.greeting_sent = False def _write_details_to_disk(self, resp: LoginResponse, homeserver) -> None: with open(self.CONFIG_FILE, "w") as f: json.dump( { 'homeserver': homeserver, # e.g. "https://matrix.example.org" 'user_id': resp.user_id, # e.g. "@user:example.org" 'device_id': resp.device_id, # device ID, 10 uppercase letters 'access_token': resp.access_token # cryptogr. access token }, f ) async def _login_first_time(self) -> None: homeserver = os.environ['MATRIX_SERVER'] user_id = os.environ['MATRIX_USERID'] pw = os.environ['MATRIX_PASSWORD'] device_name = os.environ['MATRIX_DEVICE'] if not os.path.exists(self.STORE_PATH): os.makedirs(self.STORE_PATH) self.client = AsyncClient( homeserver, user_id, store_path=self.STORE_PATH, config=self.client_config, ssl=(os.environ['MATRIX_SSLVERIFY'] == 'True'), ) resp = await self.client.login(password=pw, device_name=device_name) if (isinstance(resp, LoginResponse)): self._write_details_to_disk(resp, homeserver) else: logging.info( f"homeserver = \"{homeserver}\"; user = \"{user_id}\"") logging.critical(f"Failed to log in: {resp}") sys.exit(1) async def _login_with_stored_config(self) -> None: if self.client: return with open(self.CONFIG_FILE, "r") as f: config = json.load(f) self.client = AsyncClient( config['homeserver'], config['user_id'], device_id=config['device_id'], store_path=self.STORE_PATH, config=self.client_config, ssl=bool(os.environ['MATRIX_SSLVERIFY']), ) self.client.restore_login( user_id=config['user_id'], device_id=config['device_id'], access_token=config['access_token'] ) async def login(self) -> None: if os.path.exists(self.CONFIG_FILE): logging.info('Logging in using stored credentials.') else: logging.info('First time use, did not find credential file.') await self._login_first_time() logging.info( f"Logged in, credentials are stored under '{self.STORE_PATH}'.") await self._login_with_stored_config() async def _message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None: logging.info(colored( f"@{room.user_name(event.sender)} in {room.display_name} | {event.body}", 'green' )) async def _sync_callback(self, response: SyncResponse) -> None: logging.info(f"We synced, token: {response.next_batch}") if not self.greeting_sent: self.greeting_sent = True greeting = f"Hi, I'm up and runnig from **{os.environ['MATRIX_DEVICE']}**, waiting for webhooks!" await self.send_message(greeting, os.environ['MATRIX_ADMIN_ROOM'], 'Webhook server') async def send_message( self, message: str, room: str, sender: str, sync: Optional[bool] = False ) -> None: if sync: await self.client.sync(timeout=3000, full_state=True) msg_prefix = "" if os.environ['DISPLAY_APP_NAME'] == 'True': msg_prefix = f"**{sender}** says: \n" content = { 'msgtype': 'm.text', 'body': f"{msg_prefix}{message}", } if os.environ['USE_MARKDOWN'] == 'True': # Markdown formatting removes YAML newlines if not padded with spaces, # and can also mess up posted data like system logs logging.debug('Markdown formatting is turned on.') content['format'] = 'org.matrix.custom.html' content['formatted_body'] = markdown( f"{msg_prefix}{message}", extensions=['extra']) await self.client.room_send( room_id=room, message_type="m.room.message", content=content, ignore_unverified_devices=True ) async def run(self) -> None: await self.login() self.client.add_event_callback(self._message_callback, RoomMessageText) self.client.add_response_callback(self._sync_callback, SyncResponse) if self.client.should_upload_keys: await self.client.keys_upload() for room in self.join_rooms: await self.client.join(room) await self.client.joined_rooms() logging.info('The Matrix client is waiting for events.') await self.client.sync_forever(timeout=300000, full_state=True)
class 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()