From 0f4d5fcee874fd1e1a05f70ee5bd08a79923f25e Mon Sep 17 00:00:00 2001 From: Michael Sasser Date: Mon, 13 Jan 2025 21:07:43 +0100 Subject: [PATCH 1/2] feat(redact): add command to redact messages using the admin API --- src/matrixctl/commands/redact/__init__.py | 20 ++ src/matrixctl/commands/redact/addon.py | 234 ++++++++++++++++++++++ src/matrixctl/commands/redact/parser.py | 106 ++++++++++ 3 files changed, 360 insertions(+) create mode 100644 src/matrixctl/commands/redact/__init__.py create mode 100644 src/matrixctl/commands/redact/addon.py create mode 100644 src/matrixctl/commands/redact/parser.py diff --git a/src/matrixctl/commands/redact/__init__.py b/src/matrixctl/commands/redact/__init__.py new file mode 100644 index 00000000..edd45631 --- /dev/null +++ b/src/matrixctl/commands/redact/__init__.py @@ -0,0 +1,20 @@ +# matrixctl +# Copyright (c) 2020-2023 Michael Sasser +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Use the modules of this package to add functionality to Matrixctl.""" + + +# vim: set ft=python : diff --git a/src/matrixctl/commands/redact/addon.py b/src/matrixctl/commands/redact/addon.py new file mode 100644 index 00000000..dcd67a06 --- /dev/null +++ b/src/matrixctl/commands/redact/addon.py @@ -0,0 +1,234 @@ +# matrixctl +# Copyright (c) 2020-2023 Michael Sasser +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Use this module to add the ``delroom`` subcommand to ``matrixctl``.""" + +from __future__ import annotations + +import json +import logging +import typing as t + +from argparse import Namespace +from time import sleep + +from matrixctl.errors import InternalResponseError +from matrixctl.handlers.api import RequestBuilder +from matrixctl.handlers.api import Response +from matrixctl.handlers.api import request +from matrixctl.handlers.yaml import YAML +from matrixctl.sanitizers import sanitize_room_identifier +from matrixctl.sanitizers import sanitize_sequence +from matrixctl.typehints import JsonDict + + +__author__: str = "Michael Sasser" +__email__: str = "Michael@MichaelSasser.org" + + +logger = logging.getLogger(__name__) + + +def addon(arg: Namespace, yaml: YAML) -> int: + """Redact events of a given user. + + 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 | t.Literal[1] = handle_arguments(arg) + if isinstance(body, int): + return body + + req: RequestBuilder = RequestBuilder( + token=yaml.get("server", "api", "token"), + domain=yaml.get("server", "api", "domain"), + path=f"/_synapse/admin/v1/user/{arg.user_id}/redact", + method="POST", + json=body, + timeout=1200, + ) + + try: + response: Response = request(req) + except InternalResponseError: + logger.exception("Could not redact events.") + 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.") + msg: str = f"The response was: {response = }" + raise InternalResponseError(msg) from e + + try: + json_response = handle_status( + yaml, + json_response["redact_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 + + +# TODO: Try to simplify this function +# ruff: noqa: C901 +def handle_status(yaml: YAML, redact_id: str) -> JsonDict: + """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"/_synapse/admin/v1/user/redact_status/{redact_id}", + method="GET", + timeout=1200.0, + ) + + # Lock messages to only print them once + msglock_scheduled: bool = False # TODO: rly needed here? + msglock_active: bool = False + + while True: + sleep(1) + try: + response: Response = request(req) + except InternalResponseError as e: + msg: str = ( + "The delete room request was probably successful but the" + " status request failed. You just have to wait a bit." + ) + raise InternalResponseError(msg) 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.", + ) + msg_: str = f"The response was: {response = }" + raise InternalResponseError(msg_) from e + + if response is not None: + logger.debug("response: %s", response) + # complete + if json_response["status"] == "completed": + print( + "Status: Complete " + "(The Events have been redacted successfully)", + ) + break + # scheduled + if json_response["status"] == "scheduled": + if not msglock_scheduled: + print( + "Status: Scheduled " + "(The Redaction Progress Will Start Soon)", + ) + msglock_scheduled = True + logger.info( + "The server is still shutting_down the room. " + "Please wait...", + ) + sleep(5) + continue + # purging + if json_response["status"] == "active": + if not msglock_active: + print("Status: Active (Redacting Events)") + msglock_active = 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 following message: %s." + ), + json_response["status"], + ) + break + break + + return json_response + + +def handle_arguments(arg: Namespace) -> JsonDict | t.Literal[1]: + """Build the parameters used for the redact request. + + Parameters + ---------- + arg : argparse.Namespace + The ``Namespace`` object of argparse's ``parse_args()`` + + Returns + ------- + body : matrixctl.typehints.JsonDict + The params. + + """ + + room_identifiers: tuple[str, ...] | t.Literal[False] | None = ( + sanitize_sequence(sanitize_room_identifier, arg.room_ids) + ) + if room_identifiers is False: + return 1 + + body: JsonDict = { + "rooms": room_identifiers, + "reason": arg.reason.strip(), + "limit": arg.limit, + } + + logger.debug("Body: %s", body) + return body + + +# vim: set ft=python : diff --git a/src/matrixctl/commands/redact/parser.py b/src/matrixctl/commands/redact/parser.py new file mode 100644 index 00000000..c9afcf2a --- /dev/null +++ b/src/matrixctl/commands/redact/parser.py @@ -0,0 +1,106 @@ +# matrixctl +# Copyright (c) 2020-2023 Michael Sasser +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Use this module to add the ``redact`` subcommand to ``matrixctl``.""" + +from __future__ import annotations + +import typing as t + +from argparse import ArgumentParser +from argparse import RawDescriptionHelpFormatter +from argparse import _SubParsersAction + +from matrixctl.addon_manager import subparser + + +__author__: str = "Michael Sasser" +__email__: str = "Michael@MichaelSasser.org" + + +@subparser +def subparser_redact(subparsers: _SubParsersAction[t.Any]) -> None: + """Create a subparser for the ``matrixctl redact`` command. + + Parameters + ---------- + subparsers : argparse._SubParsersAction of typing.Any + The object which is returned by + ``parser.add_subparsers()``. + + Returns + ------- + None + + """ + parser: ArgumentParser = subparsers.add_parser( + "redact", + help="Redact events", + formatter_class=RawDescriptionHelpFormatter, + description=( + "This command allows an admin to redact the events of a given" + " user. There are no restrictions on redactions for a local user." + " By default, we puppet the user who sent the message to redact" + " it themselves. Redactions for non-local users are issued using" + " the admin user, and will fail in rooms where the admin user is" + " not admin/does not have the specified power level to issue" + " redactions." + ), + ) + parser.add_argument( + "user_id", + type=str, + help=( + "The fully qualified MXID of the user: for example," + ' "@user:server.com"' + ), + ) + parser.add_argument( + "-r", + "--room_ids", + nargs="+", + type=str, + help=( + "A list of rooms to redact the user's events in. If an empty list" + "is provided all events in all rooms the user is a member of will" + " be redacted" + ), + ) + parser.add_argument( + "-t", + "--reason", + type=str, + help=( + 'A Reason the redaction is being requested, ie "spam", "abuse", ' + "etc. This will be included in each redaction event, and be" + " visible to users" + ), + ) + parser.add_argument( + "-l", + "--limit", + type=int, + default=1000, + help=( + "A limit on the number of the user's events to search for ones" + " that can be redacted (events are redacted newest to oldest)" + " in each room, defaults to 1000 if not provided" + ), + ) + parser.set_defaults(addon="redact") + + +# vim: set ft=python : From 165da31f938511cd9a7ea229099b5dc12c76b64c Mon Sep 17 00:00:00 2001 From: Michael Sasser Date: Mon, 13 Jan 2025 21:24:57 +0100 Subject: [PATCH 2/2] doc(redact): add documentation --- README.md | 1 + docs/source/contributer_documentation/commands.rst | 13 +++++++++++++ docs/source/contributer_documentation/handlers.rst | 8 +++++++- src/matrixctl/commands/redact/parser.py | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6f0a078..e2606e50 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Commands: make-room-admin Grant a user the highest power level available to a local user in this room purge-history Purge historic events from the database purge-remote-media Purge cached, remote media + redact Redact the events of a given user report Get a report event by report identifier reports Lists reported events rooms List rooms diff --git a/docs/source/contributer_documentation/commands.rst b/docs/source/contributer_documentation/commands.rst index bd449bca..3d74bd35 100644 --- a/docs/source/contributer_documentation/commands.rst +++ b/docs/source/contributer_documentation/commands.rst @@ -412,6 +412,19 @@ purge-remote-media :undoc-members: :show-inheritance: +redact +------ + +.. automodule:: matrixctl.commands.redact.parser + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: matrixctl.commands.redact.addon + :members: + :undoc-members: + :show-inheritance: + delete-local-media ------------------ diff --git a/docs/source/contributer_documentation/handlers.rst b/docs/source/contributer_documentation/handlers.rst index 0f7e4cd1..17fa2eba 100644 --- a/docs/source/contributer_documentation/handlers.rst +++ b/docs/source/contributer_documentation/handlers.rst @@ -77,6 +77,12 @@ Database :undoc-members: :show-inheritance: +Rows +---- + +.. automodule:: matrixctl.handlers.rows + :members: + :undoc-members: + :show-inheritance: -.. vim: set ft=rst : diff --git a/src/matrixctl/commands/redact/parser.py b/src/matrixctl/commands/redact/parser.py index c9afcf2a..0276d9ef 100644 --- a/src/matrixctl/commands/redact/parser.py +++ b/src/matrixctl/commands/redact/parser.py @@ -48,7 +48,7 @@ def subparser_redact(subparsers: _SubParsersAction[t.Any]) -> None: """ parser: ArgumentParser = subparsers.add_parser( "redact", - help="Redact events", + help="Redact the events of a given user", formatter_class=RawDescriptionHelpFormatter, description=( "This command allows an admin to redact the events of a given"