def decrypt_sync_body(self, body, ignore_failures=True): # type: (Dict[Any, Any], bool) -> Dict[Any, Any] """Go through a json sync response and decrypt megolm encrypted events. Args: body (Dict[Any, Any]): The dictionary of a Sync response. Returns the json response with decrypted events. """ logger.info("Decrypting sync") self.handle_to_device_from_sync_body(body) for room_id, room_dict in body["rooms"]["join"].items(): try: if not self.rooms[room_id].encrypted: logger.info("Room {} is not encrypted skipping...".format( self.rooms[room_id].display_name)) continue except KeyError: # We don't know if the room is encrypted or not, probably # because the client sync stream got to join the room before the # pan sync stream did. Let's assume that the room is encrypted. pass for event in room_dict["timeline"]["events"]: if "type" not in event: continue if event["type"] != "m.room.encrypted": continue self.pan_decrypt_event(event, room_id, ignore_failures) return body
async def loop(self): """Start a loop that runs forever and keeps on syncing with the server. The loop can be stopped with the stop_loop() method. """ self.loop_running = True self.loop_stopped.clear() logger.info(f"Starting sync loop for {self.user_id}") while self.loop_running: if not self.logged_in: # TODO login pass # TODO use user lazy loading here response = await self.sync(30000) if self.should_upload_keys: await self.keys_upload() if self.should_query_keys: key_query_response = await self.keys_query() if isinstance(key_query_response, KeysQueryResponse): self.verify_devices(key_query_response.changed) if not isinstance(response, SyncResponse): # TODO error handling pass self.synced.set() self.synced.clear() logger.info("Stopping the sync loop") self.loop_stopped.set()
def start_loop(self, loop_sleep_time=100): """Start a loop that runs forever and keeps on syncing with the server. The loop can be stopped with the stop_loop() method. """ assert not self.task logger.info(f"Starting sync loop for {self.user_id}") loop = asyncio.get_event_loop() if INDEXING_ENABLED: self.history_fetcher_task = loop.create_task(self.fetcher_loop()) timeout = 30000 sync_filter = {"room": {"state": {"lazy_load_members": True}}} next_batch = self.pan_store.load_token(self.server_name, self.user_id) self.last_sync_token = next_batch # We don't store any room state so initial sync needs to be with the # full_state parameter. Subsequent ones are normal. task = loop.create_task( self.sync_forever( timeout, sync_filter, full_state=True, since=next_batch, loop_sleep_time=loop_sleep_time, ) ) self.task = task return task
async def sync_tasks(self, response): if self.index: await self.index.commit_events() if self.last_sync_token == self.next_batch: return self.last_sync_token = self.next_batch self.pan_store.save_token(self.server_name, self.user_id, self.next_batch) for room_id, room_info in response.rooms.join.items(): if room_info.timeline.limited: room = self.rooms[room_id] if not room.encrypted and self.pan_conf.index_encrypted_only: continue logger.info( "Room {} had a limited timeline, queueing " "room for history fetching.".format(room.display_name) ) task = FetchTask(room_id, room_info.timeline.prev_batch) self.pan_store.save_fetcher_task(self.server_name, self.user_id, task) await self.history_fetch_queue.put(task) self.new_fetch_task.set() self.new_fetch_task.clear()
def pan_decrypt_event(self, event_dict, room_id=None, ignore_failures=True): # type: (Dict[Any, Any], Optional[str], bool) -> (bool) event = Event.parse_encrypted_event(event_dict) if not isinstance(event, MegolmEvent): logger.warn( "Encrypted event is not a megolm event:" "\n{}".format(pformat(event_dict)) ) return False if not event.room_id: event.room_id = room_id try: decrypted_event = self.decrypt_event(event) logger.info("Decrypted event: {}".format(decrypted_event)) event_dict.update(decrypted_event.source) event_dict["decrypted"] = True event_dict["verified"] = decrypted_event.verified return True except EncryptionError as error: logger.warn(error) if ignore_failures: event_dict.update(self.unable_to_decrypt) else: raise return False
async def loop_stop(self): """Stop the client loop.""" logger.info("Stopping the sync loop") if self.task and not self.task.done(): self.task.cancel() try: await self.task except KeyboardInterrupt: pass self.task = None if self.history_fetcher_task and not self.history_fetcher_task.done(): self.history_fetcher_task.cancel() try: await self.history_fetcher_task except KeyboardInterrupt: pass self.history_fetcher_task = None if isinstance(self.store, SqliteQueueDatabase): self.store.close() self.history_fetch_queue = asyncio.Queue()
def decrypt_messages_body(self, body, ignore_failures=True): # type: (Dict[Any, Any], bool) -> Dict[Any, Any] """Go through a messages response and decrypt megolm encrypted events. Args: body (Dict[Any, Any]): The dictionary of a Sync response. Returns the json response with decrypted events. """ if "chunk" not in body: return body logger.info("Decrypting room messages") for event in body["chunk"]: if "type" not in event: continue if event["type"] != "m.room.encrypted": logger.debug("Event is not encrypted: " "\n{}".format(pformat(event))) continue self.pan_decrypt_event(event, ignore_failures=ignore_failures) return body
async def key_verification_cb(self, event): logger.info("Received key verification event: {}".format(event)) if isinstance(event, KeyVerificationStart): logger.info(f"{event.sender} via {event.from_device} has started " f"a key verification process.") message = InviteSasSignal(self.user_id, event.sender, event.from_device, event.transaction_id) await self.send_message(message) elif isinstance(event, KeyVerificationKey): sas = self.key_verifications.get(event.transaction_id, None) if not sas: return device = sas.other_olm_device emoji = sas.get_emoji() message = ShowSasSignal(self.user_id, device.user_id, device.id, sas.transaction_id, emoji) await self.send_message(message) elif isinstance(event, KeyVerificationMac): sas = self.key_verifications.get(event.transaction_id, None) if not sas: return device = sas.other_olm_device if sas.verified: await self.send_message( SasDoneSignal(self.user_id, device.user_id, device.id, sas.transaction_id)) await self.send_update_device(device)
async def start_panta_client(self, access_token, user, user_id, password): client = Client(user_id, access_token) self.client_info[access_token] = client if user_id in self.panta_clients: logger.info(f"Background sync client already exists for {user_id}," f" not starting new one") return panta_client = PantaClient( self.homeserver, user, store_path=self.data_dir, ssl=self.ssl, proxy=self.proxy ) response = await panta_client.login(password, "pantalaimon") if not isinstance(response, LoginResponse): await panta_client.close() return logger.info(f"Succesfully started new background sync client for " f"{user_id}") self.panta_clients[user_id] = panta_client loop = asyncio.get_event_loop() loop.create_task(panta_client.loop())
async def download(self, request): server_name = request.match_info["server_name"] media_id = request.match_info["media_id"] file_name = request.match_info.get("file_name") try: media_info = self.media_info[(server_name, media_id)] except KeyError: media_info = self.store.load_media(self.name, server_name, media_id) if not media_info: logger.info( f"No media info found for {server_name}/{media_id}") return await self.forward_to_web(request) self.media_info[(server_name, media_id)] = media_info try: key = media_info.key["k"] hash = media_info.hashes["sha256"] except KeyError: logger.warn( f"Media info for {server_name}/{media_id} doesn't contain a key or hash." ) return await self.forward_to_web(request) if not self.pan_clients: return await self.forward_to_web(request) client = next(iter(self.pan_clients.values())) try: response = await client.download(server_name, media_id, file_name) except ClientConnectionError as e: return web.Response(status=500, text=str(e)) if not isinstance(response, DownloadResponse): return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=await response.transport_response.read(), ) logger.info(f"Decrypting media {server_name}/{media_id}") loop = asyncio.get_running_loop() with concurrent.futures.ProcessPoolExecutor() as pool: decrypted_file = await loop.run_in_executor( pool, decrypt_attachment, response.body, key, hash, media_info.iv) return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=decrypted_file, )
def __attrs_post_init__(self): loop = asyncio.get_event_loop() self.homeserver_url = self.homeserver.geturl() self.hostname = self.homeserver.hostname self.store = PanStore(self.data_dir) accounts = self.store.load_users(self.name) self.media_info = self.store.load_media(self.name) for user_id, device_id in accounts: if self.conf.keyring: try: token = keyring.get_password( "pantalaimon", f"{user_id}-{device_id}-token" ) except RuntimeError as e: logger.error(e) else: token = self.store.load_access_token(user_id, device_id) if not token: logger.warn( f"Not restoring client for {user_id} {device_id}, " f"missing access token." ) continue logger.info(f"Restoring client for {user_id} {device_id}") pan_client = PanClient( self.name, self.store, self.conf, self.homeserver_url, self.send_queue, user_id, device_id, store_path=self.data_dir, ssl=self.ssl, proxy=self.proxy, store_class=self.client_store_class, media_info=self.media_info, ) pan_client.user_id = user_id pan_client.access_token = token pan_client.load_store() self.pan_clients[user_id] = pan_client loop.create_task( self.send_ui_message( UpdateUsersMessage(self.name, user_id, pan_client.device_id) ) ) loop.create_task(pan_client.send_update_devices(pan_client.device_store)) pan_client.start_loop()
def verify_devices(self, changed_devices): # Verify new devices automatically for now. for user_id, device_dict in changed_devices.items(): for device in device_dict.values(): if device.deleted: continue logger.info("Automatically verifying device {} of " "user {}".format(device.id, user_id)) self.verify_device(device)
async def start_pan_client(self, access_token, user, user_id, password): client = ClientInfo(user_id, access_token) self.client_info[access_token] = client self.store.save_server_user(self.name, user_id) if user_id in self.pan_clients: logger.info( f"Background sync client already exists for {user_id}," f" not starting new one" ) return pan_client = PanClient( self.name, self.store, self.conf, self.homeserver_url, self.send_queue, user_id, store_path=self.data_dir, ssl=self.ssl, proxy=self.proxy, store_class=self.client_store_class, media_info=self.media_info, ) response = await pan_client.login(password, "pantalaimon") if not isinstance(response, LoginResponse): await pan_client.close() return logger.info(f"Succesfully started new background sync client for " f"{user_id}") await self.send_ui_message( UpdateUsersMessage(self.name, user_id, pan_client.device_id) ) self.pan_clients[user_id] = pan_client if self.conf.keyring: try: keyring.set_password( "pantalaimon", f"{user_id}-{pan_client.device_id}-token", pan_client.access_token, ) except RuntimeError as e: logger.error(e) else: self.store.save_access_token( user_id, pan_client.device_id, pan_client.access_token ) pan_client.start_loop()
async def undecrypted_event_cb(self, room, event): logger.info("Unable to decrypt event from {} via {}.".format( event.sender, event.device_id)) if event.session_id not in self.outgoing_key_requests: logger.info("Requesting room key for undecrypted event.") # TODO we may want to retry this try: await self.request_room_key(event) except ClientConnectionError: pass
async def _unverify_device(self, message_id, client, device): ret = client.unverify_device(device) if ret: msg = (f"Device {device.id} of user " f"{device.user_id} succesfully unverified.") await client.send_update_device(device) else: msg = f"Device {device.id} of user " f"{device.user_id} already unverified." logger.info(msg) await self.send_response(message_id, client.user_id, "m.ok", msg)
async def upload(self, request): file_name = request.query.get("filename", "") content_type = request.headers.get("Content-Type", "application/octet-stream") client = next(iter(self.pan_clients.values())) body = await request.read() try: response, maybe_keys = await client.upload( data_provider=BufferedReader(BytesIO(body)), content_type=content_type, filename=file_name, encrypt=True, filesize=len(body), ) if not isinstance(response, UploadResponse): return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=await response.transport_response.read(), ) self.store.save_upload(self.name, response.content_uri, file_name, content_type) mxc = urlparse(response.content_uri) mxc_server = mxc.netloc.strip("/") mxc_path = mxc.path.strip("/") logger.info( f"Adding media info for {mxc_server}/{mxc_path} to the store") media_info = MediaInfo( mxc_server, mxc_path, maybe_keys["key"], maybe_keys["iv"], maybe_keys["hashes"], ) self.store.save_media(self.name, media_info) return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=await response.transport_response.read(), ) except ClientConnectionError as e: return web.Response(status=500, text=str(e)) except SendRetryError as e: return web.Response(status=503, text=str(e))
async def login(self, request): try: body = await request.json() except (JSONDecodeError, ContentTypeError): # After a long debugging session the culprit ended up being aiohttp # and a similar bug to # https://github.com/aio-libs/aiohttp/issues/2277 but in the server # part of aiohttp. The bug is fixed in the latest master of # aiohttp. # Return 500 here for now since quaternion doesn't work otherwise. # After aiohttp 4.0 gets replace this with a 400 M_NOT_JSON # response. return web.json_response( { "errcode": "M_NOT_JSON", "error": "Request did not contain valid JSON.", }, status=500, ) user = self._get_login_user(body) password = body.get("password", "") logger.info(f"New user logging in: {user}") try: response = await self.forward_request(request) except ClientConnectionError as e: return web.Response(status=500, text=str(e)) try: json_response = await response.json() except (JSONDecodeError, ContentTypeError): json_response = None if response.status == 200 and json_response: user_id = json_response.get("user_id", None) access_token = json_response.get("access_token", None) device_id = json_response.get("device_id", None) if user_id and access_token: logger.info(f"User: {user} succesfully logged in, starting " f"a background sync client.") await self.start_pan_client(access_token, user, user_id, password, device_id) return web.Response( status=response.status, content_type=response.content_type, headers=CORS_HEADERS, body=await response.read(), )
def decrypt_sync_body(self, body): # type: (Dict[Any, Any]) -> Dict[Any, Any] """Go through a json sync response and decrypt megolm encrypted events. Args: body (Dict[Any, Any]): The dictionary of a Sync response. Returns the json response with decrypted events. """ for room_id, room_dict in body["rooms"]["join"].items(): try: if not self.rooms[room_id].encrypted: logger.info("Room {} is not encrypted skipping...".format( self.rooms[room_id].display_name)) continue except KeyError: logger.info("Unknown room {} skipping...".format(room_id)) continue for event in room_dict["timeline"]["events"]: if event["type"] != "m.room.encrypted": logger.info("Event is not encrypted: " "\n{}".format(pformat(event))) continue parsed_event = RoomEncryptedEvent.parse_event(event) parsed_event.room_id = room_id if not isinstance(parsed_event, MegolmEvent): logger.warn("Encrypted event is not a megolm event:" "\n{}".format(pformat(event))) continue try: decrypted_event = self.decrypt_event(parsed_event) logger.info("Decrypted event: {}".format(decrypted_event)) event["type"] = "m.room.message" # TODO support other event types # This should be best done in nio, modify events so they # keep the dictionary from which they are built in a source # attribute. event["content"] = { "msgtype": "m.text", "body": decrypted_event.body } if decrypted_event.formatted_body: event["content"]["formatted_body"] = ( decrypted_event.formatted_body) event["content"]["format"] = decrypted_event.format event["decrypted"] = True event["verified"] = decrypted_event.verified except EncryptionError as error: logger.warn(error) continue return body
def pan_decrypt_event(self, event_dict, room_id=None, ignore_failures=True): # type: (Dict[Any, Any], Optional[str], bool) -> (bool) event = Event.parse_encrypted_event(event_dict) if not isinstance(event, MegolmEvent): logger.warn("Encrypted event is not a megolm event:" "\n{}".format(pformat(event_dict))) return False if not event.room_id: event.room_id = room_id try: decrypted_event = self.decrypt_event(event) logger.debug("Decrypted event: {}".format(decrypted_event)) logger.info("Decrypted event from {} in {}, event id: {}".format( decrypted_event.sender, decrypted_event.room_id, decrypted_event.event_id, )) if isinstance(decrypted_event, RoomEncryptedMedia): self.store_event_media(decrypted_event) decrypted_event.source["content"]["url"] = decrypted_event.url if decrypted_event.thumbnail_url: decrypted_event.source["content"]["info"][ "thumbnail_url"] = decrypted_event.thumbnail_url event_dict.update(decrypted_event.source) event_dict["decrypted"] = True event_dict["verified"] = decrypted_event.verified return True except EncryptionError as error: logger.warn(error) if ignore_failures: event_dict.update(self.unable_to_decrypt) else: raise return False
async def key_request_cb(self, event): if isinstance(event, RoomKeyRequest): logger.info(f"{event.sender} via {event.requesting_device_id} has " f" requested room keys from us.") message = KeyRequestMessage(self.user_id, event) await self.send_message(message) elif isinstance(event, RoomKeyRequestCancellation): logger.info(f"{event.sender} via {event.requesting_device_id} has " f" canceled its key request.") message = KeyRequestMessage(self.user_id, event) await self.send_message(message) else: assert False
async def _find_client(self, access_token): client_info = self.client_info.get(access_token, None) if not client_info: async with aiohttp.ClientSession() as session: try: method, path = Api.whoami(access_token) resp = await session.request( method, self.homeserver_url + path, proxy=self.proxy, ssl=self.ssl, ) except ClientConnectionError: return None if resp.status != 200: return None try: body = await resp.json() except (JSONDecodeError, ContentTypeError): return None try: user_id = body["user_id"] except KeyError: return None if user_id not in self.pan_clients: logger.warn( f"User {user_id} doesn't have a matching pan " f"client." ) return None logger.info( f"Homeserver confirmed valid access token " f"for user {user_id}, caching info." ) client_info = ClientInfo(user_id, access_token) self.client_info[access_token] = client_info client = self.pan_clients.get(client_info.user_id, None) return client
async def _load_decrypted_file(self, server_name, media_id, file_name): try: media_info = self.media_info[(server_name, media_id)] except KeyError: media_info = self.store.load_media(self.name, server_name, media_id) if not media_info: logger.info( f"No media info found for {server_name}/{media_id}") return None, None self.media_info[(server_name, media_id)] = media_info try: key = media_info.key["k"] hash = media_info.hashes["sha256"] except KeyError as e: logger.warn( f"Media info for {server_name}/{media_id} doesn't contain a key or hash." ) raise e if not self.pan_clients: return None, None client = next(iter(self.pan_clients.values())) try: response = await client.download(server_name, media_id, file_name) except ClientConnectionError as e: raise e if not isinstance(response, DownloadResponse): return response, None logger.info(f"Decrypting media {server_name}/{media_id}") loop = asyncio.get_running_loop() with concurrent.futures.ProcessPoolExecutor() as pool: decrypted_file = await loop.run_in_executor( pool, decrypt_attachment, response.body, key, hash, media_info.iv) return response, decrypted_file
def store_media_cb(self, room, event): try: mxc = urlparse(event.url) except ValueError: return if mxc is None: return mxc_server = mxc.netloc.strip("/") mxc_path = mxc.path.strip("/") logger.info( f"Adding media info for {mxc_server}/{mxc_path} to the store") media = MediaInfo(mxc_server, mxc_path, event.key, event.iv, event.hashes) self.media_info[(mxc_server, mxc_path)] = media self.pan_store.save_media(self.server_name, media)
def decrypt_sync_body(self, body, ignore_failures=True): # type: (Dict[Any, Any], bool) -> Dict[Any, Any] """Go through a json sync response and decrypt megolm encrypted events. Args: body (Dict[Any, Any]): The dictionary of a Sync response. Returns the json response with decrypted events. """ logger.info("Decrypting sync") for room_id, room_dict in body["rooms"]["join"].items(): try: if not self.rooms[room_id].encrypted: logger.info( "Room {} is not encrypted skipping...".format( self.rooms[room_id].display_name ) ) continue except KeyError: logger.info("Unknown room {} skipping...".format(room_id)) continue for event in room_dict["timeline"]["events"]: if "type" not in event: continue if event["type"] != "m.room.encrypted": continue self.pan_decrypt_event(event, room_id, ignore_failures) return body
async def decrypt_body(self, client, body, sync=True): """Try to decrypt the a sync or messages body.""" decryption_method = (client.decrypt_sync_body if sync else client.decrypt_messages_body) async def decrypt_loop(client, body): while True: try: logger.info("Trying to decrypt sync") return decryption_method(body, ignore_failures=False) except EncryptionError: logger.info("Error decrypting sync, waiting for next pan " "sync") await client.synced.wait(), logger.info("Pan synced, retrying decryption.") try: return await asyncio.wait_for(decrypt_loop(client, body), timeout=self.decryption_timeout) except asyncio.TimeoutError: logger.info("Decryption attempt timed out, decrypting with " "failures") return decryption_method(body, ignore_failures=True)
async def decrypt_loop(client, body): while True: try: logger.info("Trying to decrypt sync") return decryption_method(body, ignore_failures=False) except EncryptionError: logger.info("Error decrypting sync, waiting for next pan " "sync") await client.synced.wait(), logger.info("Pan synced, retrying decryption.")
async def send_message(self, request): access_token = self.get_access_token(request) if not access_token: return self._missing_token client = await self._find_client(access_token) if not client: return self._unknown_token room_id = request.match_info["room_id"] # The room is not in the joined rooms list, just forward it. try: room = client.rooms[room_id] encrypt = room.encrypted except KeyError: return await self.forward_to_web(request, token=client.access_token) # Don't encrypt reactions for now - they are weird and clients # need to support them like this. # TODO: Fix when MSC1849 is fully supported by clients. if request.match_info["event_type"] == "m.reaction": encrypt = False # The room isn't encrypted just forward the message. if not encrypt: return await self.forward_to_web(request, token=client.access_token) msgtype = request.match_info["event_type"] txnid = request.match_info.get("txnid", uuid4()) try: content = await request.json() except (JSONDecodeError, ContentTypeError): return self._not_json async def _send(ignore_unverified=False): try: response = await client.room_send(room_id, msgtype, content, txnid, ignore_unverified) return web.Response( status=response.transport_response.status, content_type=response.transport_response.content_type, headers=CORS_HEADERS, body=await response.transport_response.read(), ) except ClientConnectionError as e: return web.Response(status=500, text=str(e)) except SendRetryError as e: return web.Response(status=503, text=str(e)) # Aquire a semaphore here so we only send out one # UnverifiedDevicesSignal sem = client.send_semaphores[room_id] async with sem: # Even though we request the full state we don't receive room # members since we're using lazy loading. The summary is for some # reason empty so nio can't know if room members are missing from # our state. Fetch the room members here instead. if not client.room_members_fetched[room_id]: try: await client.joined_members(room_id) client.room_members_fetched[room_id] = True except ClientConnectionError as e: return web.Response(status=500, text=str(e)) try: return await _send(self.conf.ignore_verification) except OlmTrustError as e: # There are unverified/unblocked devices in the room, notify # the UI thread about this and wait for a response. queue = asyncio.Queue() client.send_decision_queues[room_id] = queue message = UnverifiedDevicesSignal(client.user_id, room_id, room.display_name) await self.send_ui_message(message) try: response = await asyncio.wait_for( queue.get(), self.unverified_send_timeout) if isinstance(response, CancelSendingMessage): # The send was canceled notify the client that sent the # request about this. info_msg = (f"Canceled message sending for room " f"{room.display_name} ({room_id}).") logger.info(info_msg) await self.send_response(response.message_id, client.user_id, "m.ok", info_msg) return web.Response(status=503, text=str(e)) elif isinstance(response, SendAnywaysMessage): # We are sending and ignoring devices along the way. info_msg = (f"Ignoring unverified devices and sending " f"message to room " f"{room.display_name} ({room_id}).") logger.info(info_msg) await self.send_response(response.message_id, client.user_id, "m.ok", info_msg) ret = await _send(True) await client.send_update_devices( client.room_devices(room_id)) return ret except asyncio.TimeoutError: # We didn't get a response to our signal, send out an error # response. return web.Response( status=503, text=(f"Room contains unverified devices and no " f"action was taken for " f"{self.unverified_send_timeout} seconds, " f"request timed out"), ) finally: client.send_decision_queues.pop(room_id)
async def start_pan_client(self, access_token, user, user_id, password, device_id=None): client = ClientInfo(user_id, access_token) self.client_info[access_token] = client self.store.save_server_user(self.name, user_id) if user_id in self.pan_clients: logger.info(f"Background sync client already exists for {user_id}," f" not starting new one") return pan_client = PanClient( self.name, self.store, self.conf, self.homeserver_url, self.send_queue, user_id, store_path=self.data_dir, ssl=self.ssl, proxy=self.proxy, store_class=self.client_store_class, media_info=self.media_info, ) if password == "": if device_id is None: logger.warn( "Empty password provided and device_id was also None, not " "starting background sync client ") return # If password is blank, we cannot login normally and must # fall back to using the provided device_id. pan_client.restore_login(user_id, device_id, access_token) else: response = await pan_client.login(password, "pantalaimon") if not isinstance(response, LoginResponse): await pan_client.close() return logger.info(f"Succesfully started new background sync client for " f"{user_id}") await self.send_ui_message( UpdateUsersMessage(self.name, user_id, pan_client.device_id)) self.pan_clients[user_id] = pan_client if self.conf.keyring: try: keyring.set_password( "pantalaimon", f"{user_id}-{pan_client.device_id}-token", pan_client.access_token, ) except RuntimeError as e: logger.error(e) else: self.store.save_access_token(user_id, pan_client.device_id, pan_client.access_token) pan_client.start_loop()
async def receive_message(self, message): client = self.pan_clients.get(message.pan_user) if isinstance( message, ( DeviceVerifyMessage, DeviceUnverifyMessage, StartSasMessage, DeviceBlacklistMessage, DeviceUnblacklistMessage, ), ): device = client.device_store[message.user_id].get( message.device_id, None) if not device: msg = (f"No device found for {message.user_id} and " f"{message.device_id}") await self.send_response(message.message_id, message.pan_user, "m.unknown_device", msg) logger.info(msg) return if isinstance(message, DeviceVerifyMessage): await self._verify_device(message.message_id, client, device) elif isinstance(message, DeviceUnverifyMessage): await self._unverify_device(message.message_id, client, device) elif isinstance(message, DeviceBlacklistMessage): await self._blacklist_device(message.message_id, client, device) elif isinstance(message, DeviceUnblacklistMessage): await self._unblacklist_device(message.message_id, client, device) elif isinstance(message, StartSasMessage): await client.start_sas(message, device) elif isinstance(message, SasMessage): if isinstance(message, AcceptSasMessage): await client.accept_sas(message) elif isinstance(message, ConfirmSasMessage): await client.confirm_sas(message) elif isinstance(message, CancelSasMessage): await client.cancel_sas(message) elif isinstance(message, ExportKeysMessage): path = os.path.abspath(os.path.expanduser(message.file_path)) logger.info(f"Exporting keys to {path}") try: await client.export_keys(path, message.passphrase) except OSError as e: info_msg = (f"Error exporting keys for {client.user_id} to" f" {path} {e}") logger.info(info_msg) await self.send_response(message.message_id, client.user_id, "m.os_error", str(e)) else: info_msg = (f"Succesfully exported keys for {client.user_id} " f"to {path}") logger.info(info_msg) await self.send_response(message.message_id, client.user_id, "m.ok", info_msg) elif isinstance(message, ImportKeysMessage): path = os.path.abspath(os.path.expanduser(message.file_path)) logger.info(f"Importing keys from {path}") try: await client.import_keys(path, message.passphrase) except (OSError, EncryptionError) as e: info_msg = (f"Error importing keys for {client.user_id} " f"from {path} {e}") logger.info(info_msg) await self.send_response(message.message_id, client.user_id, "m.os_error", str(e)) else: info_msg = (f"Succesfully imported keys for {client.user_id} " f"from {path}") logger.info(info_msg) await self.send_response(message.message_id, client.user_id, "m.ok", info_msg) elif isinstance(message, UnverifiedResponse): client = self.pan_clients[message.pan_user] if message.room_id not in client.send_decision_queues: msg = (f"No send request found for user {message.pan_user} " f"and room {message.room_id}.") await self.send_response(message.message_id, message.pan_user, "m.unknown_request", msg) return queue = client.send_decision_queues[message.room_id] await queue.put(message) elif isinstance(message, (ContinueKeyShare, CancelKeyShare)): client = self.pan_clients[message.pan_user] await client.handle_key_request_message(message)
def __init__( self, server_name, pan_store, pan_conf, homeserver, queue=None, user_id="", device_id="", store_path="", config=None, ssl=None, proxy=None, store_class=None, ): config = config or AsyncClientConfig( store=store_class or SqliteStore, store_name="pan.db" ) super().__init__(homeserver, user_id, device_id, store_path, config, ssl, proxy) index_dir = os.path.join(store_path, server_name, user_id) try: os.makedirs(index_dir) except OSError: pass self.server_name = server_name self.pan_store = pan_store self.pan_conf = pan_conf if INDEXING_ENABLED: logger.info("Indexing enabled.") from pantalaimon.index import IndexStore self.index = IndexStore(self.user_id, index_dir) else: logger.info("Indexing disabled.") self.index = None self.task = None self.queue = queue # Those two events are mainly used for testing. self.new_fetch_task = asyncio.Event() self.fetch_loop_event = asyncio.Event() self.room_members_fetched = defaultdict(bool) self.send_semaphores = defaultdict(asyncio.Semaphore) self.send_decision_queues = dict() # type: asyncio.Queue self.last_sync_token = None self.history_fetcher_task = None self.history_fetch_queue = asyncio.Queue() self.add_to_device_callback(self.key_verification_cb, KeyVerificationEvent) self.add_to_device_callback( self.key_request_cb, (RoomKeyRequest, RoomKeyRequestCancellation) ) self.add_event_callback(self.undecrypted_event_cb, MegolmEvent) if INDEXING_ENABLED: self.add_event_callback( self.store_message_cb, ( RoomMessageText, RoomMessageMedia, RoomEncryptedMedia, RoomTopicEvent, RoomNameEvent, ), ) self.add_response_callback(self.keys_query_cb, KeysQueryResponse) self.add_response_callback(self.sync_tasks, SyncResponse)