diff --git a/doc/source/index_cli_reference.rst b/doc/source/index_cli_reference.rst
index 51ae6d7d..d64ba248 100644
--- a/doc/source/index_cli_reference.rst
+++ b/doc/source/index_cli_reference.rst
@@ -15,3 +15,4 @@ Command Line Reference
synadm.cli.matrix
synadm.cli.regtok
synadm.cli.notice
+ synadm.cli.raw
diff --git a/doc/source/synadm.cli.raw.rst b/doc/source/synadm.cli.raw.rst
new file mode 100644
index 00000000..b386fdd2
--- /dev/null
+++ b/doc/source/synadm.cli.raw.rst
@@ -0,0 +1,6 @@
+Raw
+====
+
+.. click:: synadm.cli.raw:raw_request_cmd
+ :prog: synadm raw
+ :nested: full
diff --git a/synadm/api.py b/synadm/api.py
index 2535e122..0abf3662 100644
--- a/synadm/api.py
+++ b/synadm/api.py
@@ -1370,3 +1370,17 @@ def notice_send(self, receivers, content_plain, content_html, paginate,
else:
data["user_id"] = receivers
return [self.query("post", "v1/send_server_notice", data=data)]
+
+ def raw_request(self, endpoint, method, data):
+ data_dict = {}
+ if method != "get":
+ self.log.debug("The data we are trying to parse and submit:")
+ self.log.debug(data)
+ try: # user provided json might be crap
+ data_dict = json.loads(data)
+ except Exception as error:
+ self.log.error("loading data: %s: %s",
+ type(error).__name__, error)
+ return None
+
+ return self.query(method, endpoint, data=data_dict)
diff --git a/synadm/cli/__init__.py b/synadm/cli/__init__.py
index 8346743b..a758c997 100644
--- a/synadm/cli/__init__.py
+++ b/synadm/cli/__init__.py
@@ -505,4 +505,4 @@ def version(helper):
# Import additional commands
-from synadm.cli import room, user, media, group, history, matrix, regtok, notice # noqa: F401, E402, E501
+from synadm.cli import room, user, media, group, history, matrix, regtok, notice, raw # noqa: F401, E402, E501
diff --git a/synadm/cli/_common.py b/synadm/cli/_common.py
new file mode 100644
index 00000000..5440fec3
--- /dev/null
+++ b/synadm/cli/_common.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# synadm
+# Copyright (C) 2020-2023 Johannes Tiefenbacher
+#
+# synadm 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.
+#
+# synadm 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 .
+"Common CLI options, option groups, helpers and utilities."
+
+import click
+
+from click_option_group import MutuallyExclusiveOptionGroup
+
+
+def common_opts_raw_command(function):
+ return click.argument(
+ "endpoint", type=str
+ )(
+ click.option(
+ "--method", "-m",
+ type=click.Choice(["get", "post", "put", "delete"]),
+ help="The HTTP method used for the request.",
+ default="get", show_default=True
+ )(function)
+ )
+
+
+data_group_raw_command = MutuallyExclusiveOptionGroup(
+ "Data",
+ help=""
+)
+
+
+def data_opts_raw_command(function):
+ return data_group_raw_command.option(
+ "--data", "-d", type=str, default='{}', show_default=True,
+ help="""The JSON string sent in the body of post, put and delete
+ requests - provided as a string. Make sure to escape it from shell
+ interpretation by using single quotes. E.g '{"key1": "value1",
+ "key2": 123}'"""
+ )(
+ data_group_raw_command.option(
+ "--data-file", "-f", type=click.File("rt"),
+ show_default=True,
+ help="""Read JSON data from file. To read from stdin use "-" as the
+ filename argument."""
+ )(function)
+ )
diff --git a/synadm/cli/matrix.py b/synadm/cli/matrix.py
index 5a64ca6b..c233b7b2 100644
--- a/synadm/cli/matrix.py
+++ b/synadm/cli/matrix.py
@@ -21,6 +21,7 @@
import click
from synadm import cli
+from synadm.cli._common import common_opts_raw_command, data_opts_raw_command
from click_option_group import optgroup, MutuallyExclusiveOptionGroup
@@ -72,28 +73,8 @@ def login_cmd(helper, user_id, password):
@matrix.command(name="raw")
-@click.argument(
- "endpoint", type=str)
-@click.option(
- "--method", "-m", type=click.Choice(["get", "post", "put", "delete"]),
- help="""The HTTP method used for the request.""",
- default="get", show_default=True)
-@optgroup.group(
- "Data input",
- cls=MutuallyExclusiveOptionGroup,
- help="")
-@optgroup.option(
- "--data", "-d", type=str, default='{}', show_default=True,
- help="""The JSON string sent in the body of post, put and delete requests -
- provided as a string. Make sure to escape it from shell interpretation by
- using single quotes. E.g '{"key1": "value1", "key2": 123}'
- """)
-@optgroup.option(
- "--data-file", "-f", type=click.File("rt"),
- show_default=True,
- help="""Read JSON data from file. To read from stdin use "-" as the
- filename argument.
- """)
+@common_opts_raw_command
+@data_opts_raw_command
@optgroup.group(
"Matrix token",
cls=MutuallyExclusiveOptionGroup,
@@ -101,43 +82,47 @@ def login_cmd(helper, user_id, password):
@optgroup.option(
"--token", "-t", type=str, envvar='MTOKEN', show_default=True,
help="""Token used for Matrix authentication instead of the configured
- admin user's token. If --token (and --prompt) option is missing, the token
- is read from environment variable $MTOKEN instead. To make sure a user's
- token does not show up in system logs, don't provide it on the shell
- directly but set $MTOKEN with shell command `read MTOKEN`.""")
+ admin user's token. If ``--token`` (and ``--prompt``) option is missing,
+ the token is read from environment variable ``$MTOKEN`` instead. To make
+ sure a user's token does not show up in system logs, don't provide it on
+ the shell directly but set ``$MTOKEN`` with shell command ``read
+ MTOKEN``.""")
@optgroup.option(
"--prompt", "-p", is_flag=True, show_default=True,
help="""Prompt for the token used for Matrix authentication. This option
always overrides $MTOKEN.""")
@click.pass_obj
def raw_request_cmd(helper, endpoint, method, data, data_file, token, prompt):
- """ Execute a raw request to the Matrix API.
+ """ Execute a custom request to the Matrix API.
The endpoint argument is the part of the URL _after_ the configured base
- URL and Matrix path (see `synadm config`). A simple get request would e.g
- look like this: `synadm matrix raw client/versions`
+ URL (actually "Synapse base URL") and "Matrix API path" (see ``synadm
+ config``). A get request could look like this: ``synadm matrix raw
+ client/versions`` URL encoding must be handled at this point. Consider
+ enabling debug outputs via synadm's global flag ``-vv``
- Use either --token or --prompt to provide a user's token and execute Matrix
- commands on their behalf. Respect the privacy of others! Be responsible!
+ Use either ``--token`` or ``--prompt`` to provide a user's token and
+ execute Matrix commands on their behalf. Respect the privacy of others!
+ Act responsible!
+ \b
The precedence rules for token reading are:
- 1. Interactive input using --prompt; 2. Set on CLI via --token string;
- 3. Read from environment variable $MTOKEN; 4. Preconfigured admin token
- set in synadm's config file.
+ 1. Interactive input using ``--prompt``;
+ 2. Set on CLI via ``--token``
+ 3. Read from environment variable ``$MTOKEN``;
+ 4. Preconfigured admin token set via ``synadm config``.
+
+ Caution: Passing secrets as CLI arguments or via environment variables is
+ not considered secure. Know what you are doing!
"""
if prompt:
token = click.prompt("Matrix token", type=str)
if data_file:
- raw_request = helper.matrix_api.raw_request(
- endpoint,
- method,
- data_file.read(),
- token=token
- )
- else:
- raw_request = helper.matrix_api.raw_request(endpoint, method, data,
- token=token)
+ data = data_file.read()
+
+ raw_request = helper.matrix_api.raw_request(endpoint, method, data,
+ token=token)
if helper.no_confirm:
if raw_request is None:
diff --git a/synadm/cli/raw.py b/synadm/cli/raw.py
new file mode 100644
index 00000000..0f1501e2
--- /dev/null
+++ b/synadm/cli/raw.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# synadm
+# Copyright (C) 2020-2023 Johannes Tiefenbacher
+#
+# synadm 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.
+#
+# synadm 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 .
+
+"""This module holds the `raw` command only."""
+
+import click
+
+from synadm import cli
+from synadm.cli._common import common_opts_raw_command, data_opts_raw_command
+
+
+@cli.root.command(name="raw")
+@common_opts_raw_command
+@data_opts_raw_command
+@click.pass_obj
+def raw_request_cmd(helper, endpoint, method, data, data_file):
+ """ Issue a custom request to the Synapse Admin API.
+
+ The endpoint argument is the part of the URL _after_ the configured
+ "Synapse base URL" and "Synapse Admin API path" (see ``synadm config``).
+ A get request to the "Query User Account API" would look like this:
+ ``synadm raw v2/users/%40testuser%3Aexample.org``. URL encoding must be
+ handled at this point. Consider enabling debug outputs via synadm's global
+ flag ``-vv``
+ """
+ if data_file:
+ data = data_file.read()
+
+ raw_request = helper.api.raw_request(endpoint, method, data)
+
+ if helper.no_confirm:
+ if raw_request is None:
+ raise SystemExit(1)
+ helper.output(raw_request)
+ else:
+ if raw_request is None:
+ click.echo("The Admin API's response was empty or JSON data "
+ "could not be loaded.")
+ raise SystemExit(1)
+ else:
+ helper.output(raw_request)