Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add raw command, a new module for common CLI options, and improve matrix raw help #129

Merged
merged 6 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/index_cli_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Command Line Reference
synadm.cli.matrix
synadm.cli.regtok
synadm.cli.notice
synadm.cli.raw
6 changes: 6 additions & 0 deletions doc/source/synadm.cli.raw.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Raw
====

.. click:: synadm.cli.raw:raw_request_cmd
:prog: synadm raw
:nested: full
14 changes: 14 additions & 0 deletions synadm/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1366,3 +1366,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)
2 changes: 1 addition & 1 deletion synadm/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
57 changes: 57 additions & 0 deletions synadm/cli/_common.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
"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)
)
71 changes: 28 additions & 43 deletions synadm/cli/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -72,72 +73,56 @@ 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,
help="")
@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:
Expand Down
55 changes: 55 additions & 0 deletions synadm/cli/raw.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

"""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)