def addon(arg: Namespace, yaml: YAML) -> int: """Change whether a user is an admin or not. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"users/@{arg.user}:{yaml.get('server', 'api','domain')}/admin", api_version="v1", method="PUT", json={"admin": arg.admin.lower() == "promote"}, ) try: request(req) except InternalResponseError: logger.error("The user could not be promoted or demoted.") return 1 return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Delete a user from the the matrix instance. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"deactivate/@{arg.user}:{yaml.get('server', 'api','domain')}", api_version="v1", method="POST", json={"erase": True}, ) try: request(req) except InternalResponseError: logger.error("The user was not deleted.") return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Add a User to the synapse instance. It runs ``ask_password()`` first. If ``ask_password()`` returns ``None`` it generates a password with ``gen_password()``. Then it gives the user a overview of the username, password and if the new user should be generated as admin (if you added the ``--admin`` argument). Next, it asks a question, if the entered values are correct with the ``ask_question`` function. If the ``ask_question`` function returns True, it continues. If not, it starts from the beginning. Depending on the ``--ansible`` switch it runs the ``adduser`` command via ansible or the API Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ passwd: str = create_user(arg.user, arg.admin) if arg.ansible: ansible_run( playbook=yaml.get("server", "ansible", "playbook"), tags="register-user", extra_vars={ "username": arg.user, "password": passwd, "admin": "yes" if arg.admin else "no", }, ) return 0 req: RequestBuilder = RequestBuilder( domain=yaml.get("server", "api", "domain"), token=yaml.get("server", "api", "token"), path=f"users/@{arg.user}:{yaml.get('server', 'api','domain')}", json={ "password": passwd, "admin": arg.admin }, method="PUT", ) try: request(req) except InternalResponseError: logger.error("The User was not added.") return 0
def handle_purge_status(yaml: YAML, purge_id: str) -> int: """Check the status of the purge history request. Parameters ---------- yaml : matrixctl.handlers.yaml.YAML The configuration file handler. purge_id: str The purge id from a purge history request. Returns ------- response: matrixctl.typehints.JsonDict, optional The response as dict, containing the status. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"purge_history_status/{purge_id}", method="GET", api_version="v1", timeout=1200.0, ) while True: sleep(1) try: response: JsonDict = request(req).json() except InternalResponseError: logger.critical( "The purge history request was successful but the status " "request failed. You just have to wait a bit." "If that happens the next time, please hand in a bug report." ) return 1 # return response if response is not None: logger.debug(f"{response=}") if response["status"] == "complete": print("Done...") return 0 if response["status"] == "failed": logger.critical( "The server returned, that the purge approach failed." ) break if response["status"] == "active": logger.info( "The server is still purging historic message content. " "Please wait..." ) sleep(5) continue break return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Join a user to an room. Notes ----- - You can only modify the membership of local users. - The the token of server administrator used to authenticate against the homeserver must be in the room and must have permission to invite users. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ # sanitize arg.room = arg.room.strip() arg.user = arg.user.strip() if not arg.room[0] in {"!", "#"} or ":" not in arg.room: logger.error( "Make sure, to use the correct room identifier or alias e.g. " "!636q39766251:domain.tld or #myroom:domain.tld") if not arg.user.startswith("@"): arg.user = f"@{arg.user}" if ":" not in arg.user: arg.user = f"{arg.user}:{yaml.get('server', 'api','domain')}" logger.debug("room = %s", arg.room) logger.debug("user = %s", arg.user) # request request_config: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"join/{arg.room}", api_version="v1", method="POST", json={"user_id": arg.user}, ) try: request(request_config) except InternalResponseError: logger.error("Unknown Error. The user was not joined to the room.") return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Delete local media. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ timestamp = handle_timestamp(arg.timestamp, arg.force) req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"media/{yaml.get('server', 'api', 'domain')}/delete", params={ "before_ts": timestamp, "keep_profiles": arg.no_keep_profiles, "size_gt": arg.greater_than, }, api_version="v1", method="POST", timeout=1200, ) try: response: Response = request(req) except InternalResponseError: logger.error("The user could not be promoted or demote.") return 1 try: json_response: JsonDict = response.json() except json.decoder.JSONDecodeError as e: logger.fatal("The JSON response could not be loaded by MatrixCtl.") raise InternalResponseError(f"The response was: {response = }") from e try: print(json.dumps(json_response, indent=4)) return 0 except json.decoder.JSONDecodeError: logger.error("Unable to process the response data to JSON.") return 1 return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Delete an empty room from the database. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ body: JsonDict = handle_arguments(arg) req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"rooms/{arg.room}", method="DELETE", api_version="v2", json=body, timeout=1200, ) try: response: Response = request(req) except InternalResponseError: logger.error("Could not delete room.") return 1 try: json_response: JsonDict = response.json() except json.decoder.JSONDecodeError as e: logger.fatal("The JSON response could not be loaded by MatrixCtl.") raise InternalResponseError(f"The response was: {response = }") from e try: json_response = handle_status(yaml, json_response["delete_id"]) except InternalResponseError as e: if e.message: logger.fatal(e.message) logger.fatal( "MatrixCtl was not able to verify the status of the request.") return 1 print(json.dumps(json_response, indent=4)) return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Upload a file or image to the matrix instance. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ file_path: Path = Path(arg.file).absolute() logger.debug(f"upload: {file_path=}") mime_types: MimeTypes = MimeTypes() file_type: str = str(mime_types.guess_type(file_path.name)[0]) logger.debug(f"upload: {file_type=}") try: with file_path.open("rb") as fp: file: bytes = fp.read() except FileNotFoundError: print("No such file found. Please check your filepath.") return 1 req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path="upload/", api_path="_matrix/media", method="POST", api_version="r0", headers={"Content-Type": file_type}, content=file, ) try: response: JsonDict = request(req).json() except InternalResponseError: logger.error("The file was not uploaded.") return 1 try: print("Content URI: ", response["content_uri"]) except KeyError as e: raise InternalResponseError( "Upload was successful, but no content_uri was found.", response) from e return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Purge historic message events from the Database. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ arg.room_id = arg.room_id.strip() request_body: dict[str, str | int] = dialog_input(arg) logger.debug(f"{request_body = }") req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"purge_history/{arg.room_id.strip()}", method="POST", api_version="v1", json=request_body, timeout=10, ) try: response: JsonDict = request(req).json() except InternalResponseError as e: with suppress(KeyError): if e.payload["errcode"] == "M_UNKNOWN": logger.critical(e.payload["error"]) return 1 logger.critical( "Something went wrong with the request. Please check your data " "again.") return 1 logger.debug(f"{response=}") return handle_purge_status(yaml, response["purge_id"])
def addon(arg: Namespace, yaml: YAML) -> int: """Send a server notice to a matrix instance. Notes ----- - It uses the synapse admin API. - Note that "server notices" must be enabled in homeserver.yaml before this API can be used. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path="send_server_notice", method="POST", api_version="v1", json={ "user_id": ( f"@{arg.username}:" f"{yaml.get('server', 'api', 'domain')}" ), "content": { "msgtype": "m.text", "body": arg.message, }, }, ) try: request(req) except InternalResponseError: logger.error("The server notice was not sent.") return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Delete a user is an admin. Notes ----- If a user does not exist it still will return ``"admin": false`` or ``No``. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"users/@{arg.user}:{yaml.get('server', 'api','domain')}/admin", api_version="v1", method="GET", ) try: response: Response = request(req) except InternalResponseError: logger.error("The user could not be checked.") return 1 response_json: JsonDict = response.json() if arg.to_json: print(json.dumps(response_json, indent=4)) return 0 print(human_readable_bool(response_json["admin"])) return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Print a table of the reported events. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"event_reports/{arg.report_id}", api_version="v1", ) try: response: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not receive the user information") return 1 try: print(json.dumps(response, indent=4)) return 0 except json.decoder.JSONDecodeError: logger.error("Unable to process the response data to JSON.") return 1 return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Grant a user room admin status. By default the server admin (the caller) is granted power, but another user can optionally be specified. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()`` yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"rooms/{arg.room}/make_room_admin", api_version="v1", method="POST", ) if arg.user is not None: req.json = {"user_id": arg.user} try: request(req) except InternalResponseError: logger.error("The user could not be promoted or demote.") return 1 return 0
def addon(_: Namespace, yaml: YAML) -> int: """Get the version of the Synapse instance. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. (Unused in this function) yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path="server_version", api_version="v1", ) try: response: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not get the server sersion.") return 1 logger.debug(f"{response=}") try: print(f"Server Version: {response['server_version']}") print(f"Python Version: {response['python_version']}") except KeyError: logger.error("MatrixCtl was not able to read the server version.") return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Print a table/json of the matrix users. This function generates and prints a table of users or uses json as output format. The table can be modified. - If you want guests in the table use the ``--with-guests`` switch. - If you want deactivated user in the table use the ``--with-deactivated`` switch. Notes ----- - Needs API version 2 (``synapse`` 1.28 or greater) to work. - API version 1 is deprecated. If you encounter problems please upgrade to the latest ``synapse`` release. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ len_domain = len(yaml.get("server", "api", "domain")) + 1 # 1 for : users: list[JsonDict] = [] next_token: int | None = None total: int | None = None # ToDo: API bool req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path="users", api_version="v2", method="GET", params={ "guests": "true" if arg.with_guests or arg.all else "false", "from": 0, "limit": arg.limit if 0 < arg.limit < 100 else 100, "deactivated": "true" if arg.with_deactivated or arg.all else "false", }, timeout=10, concurrent_limit=yaml.get("server", "api", "concurrent_limit"), ) try: response: Response = request(req) except InternalResponseError: logger.critical("Could not get the data do build the user table.") return 1 response_json: JsonDict = response.json() users += response_json["users"] with suppress(KeyError): # Done: No more users next_token = int(response_json["next_token"]) total = int(response_json["total"]) if 0 < arg.limit < total: total = arg.limit # New group to not suppress KeyError in here if next_token is not None and total is not None and total > 100: async_responses = request( generate_worker_configs(req, next_token, total), ) for async_response in async_responses: users_list = async_response.json()["users"] for user in users_list: users.append(user) if arg.to_json: print(json.dumps(users, indent=4)) else: for line in to_table(users, len_domain): print(line) print(f"Total number of users: {len(users)}") return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Print a table/json of the reported events. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ reports: list[JsonDict] = [] next_token: int | None = None total: int | None = None # ToDo: API bool req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path="event_reports", api_version="v1", params={ "from": 0, "limit": arg.limit if 0 < arg.limit < 100 else 100, }, concurrent_limit=yaml.get("server", "api", "concurrent_limit"), ) try: response: Response = request(req) except InternalResponseError: logger.critical("Could not get the data do build the user table.") return 1 response_json: JsonDict = response.json() reports += response_json["event_reports"] with suppress(KeyError): # Done: No more users next_token = int(response_json["next_token"]) total = int(response_json["total"]) if 0 < arg.limit < total: total = arg.limit # New group to not suppress KeyError in here if next_token is not None and total is not None and total > 100: async_responses = request( generate_worker_configs(req, next_token, total), ) for async_response in async_responses: reports_list = async_response.json()["event_reports"] for report in reports_list: reports.append(report) if arg.to_json: print(json.dumps(reports, indent=4)) else: for line in to_table(reports): print(line) return 0
def addon(arg: Namespace, yaml: YAML) -> int: """Generate a table of the matrix rooms. Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ rooms: list[JsonDict] = [] next_token: int | None = None total: int | None = None req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path="rooms", api_version="v1", params={"limit": arg.limit if 0 < arg.limit < 100 else 100}, concurrent_limit=yaml.get("server", "api", "concurrent_limit"), ) if arg.filter: req.params["search_term"] = arg.filter if arg.reverse: req.params["dir"] = "b" if arg.order_by_size: req.params["order_by"] = "size" try: response: Response = request(req) except InternalResponseError: logger.critical("Could not get the user table.") return 1 response_json: JsonDict = response.json() rooms += response_json["rooms"] with suppress(KeyError): # Done: No more users next_token = int(response_json["next_batch"]) total = int(response_json["total_rooms"]) if 0 < arg.limit < total: total = arg.limit # New group to not suppress KeyError in here if next_token is not None and total is not None and total > 100: async_responses = request( generate_worker_configs(req, next_token, total), ) for async_response in async_responses: users_list = async_response.json()["rooms"] for room in users_list: rooms.append(room) generate_output( filter_empty_rooms(rooms) if arg.empty else rooms, arg.to_json, ) return 0
def addon(arg: Namespace, yaml: YAML) -> int: """List information about an registered user. It uses the admin API to get a python dictionary with the information. The ``generate_user_tables`` function makes the information human readable. Examples -------- .. code-block:: console $ matrixctl user dwight User: +----------------------------+--------------------------------------+ | Name | dwight | | Password Hash | $2b$12$9DUNderm1ffL1NincPap3RC | | | ompaNY1725.slOUghAvEnu5cranT0n | | Guest | False | | Admin | True | | Consent Version | | | Consent Server Notice Sent | | | Appservice Id | | | Creation Ts | 2020-04-14 13:04:21 | | User Type | | | Deactivated | False | | Displayname | Dwight Schrute | | Avatar Url | mxc://dunder-mifflin.com/sCr4 | | | nt0nsr4ng13rW45Cr33d | +----------------------------+--------------------------------------+ Threepid: +--------------+-----------------------------------+ | Medium | email | | Address | [email protected] | | Validated At | 2020-04-14 15:30:21.123000 | | Added At | 2020-04-14 15:29:19.100000 | +--------------+-----------------------------------+ If the user does not exist, the return looks like: Parameters ---------- arg : argparse.Namespace The ``Namespace`` object of argparse's ``parse_args()``. yaml : matrixctl.handlers.yaml.YAML The configuration file handler. Returns ------- err_code : int Non-zero value indicates error code, or zero on success. """ len_domain = len(yaml.get("server", "api", "domain")) + 1 req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f'users/@{arg.user}:{yaml.get("server", "api","domain")}', ) try: user_dict: JsonDict = request(req).json() except InternalResponseError: logger.critical("Could not receive the user information") return 1 if arg.to_json: print(json.dumps(user_dict, indent=4)) else: for line in to_table(user_dict, len_domain): print(line) return 0
def handle_status(yaml: YAML, delete_id: str) -> JsonDict: # noqa: C901 """Handle the status of a delete room request. Parameters ---------- yaml : matrixctl.handlers.yaml.YAML The configuration file handler. delete_id: str The delete id of a delete room request. Returns ------- response: matrixctl.typehints.JsonDict, optional The response as dict, containing the status. """ req: RequestBuilder = RequestBuilder( token=yaml.get("server", "api", "token"), domain=yaml.get("server", "api", "domain"), path=f"rooms/delete_status/{delete_id}", method="GET", api_version="v2", timeout=1200.0, ) # Lock messages to only print them once msglock_shutting_down: bool = False msglock_purging: bool = False while True: sleep(1) try: response: Response = request(req) except InternalResponseError as e: raise InternalResponseError( "The delete room request was probably successful but the" " status request failed. You just have to wait a bit.") from e try: json_response: JsonDict = response.json() except json.decoder.JSONDecodeError as e: logger.fatal( "The JSON status response could not be loaded by MatrixCtl.") raise InternalResponseError( f"The response was: {response = }") from e if response is not None: logger.debug(f"{response=}") # complete if json_response["status"] == "complete": print( "Status: Complete (the room has been deleted successfully") break # shutting_down if json_response["status"] == "shutting_down": if not msglock_shutting_down: print( "Status: Shutting Down (removing users from the room)") msglock_shutting_down = True logger.info("The server is still shutting_down the room. " "Please wait...") sleep(5) continue # purging if json_response["status"] == "purging": if not msglock_purging: print( "Status: Purging (purging the room and event data from" " database)") msglock_purging = True logger.info( "The server is still purging the room. Please wait...") sleep(5) continue # failed if json_response["status"] == "failed": logger.critical( "The server returned, that the approach failed with the" f" following message: {json_response['status']}.") break break return json_response