Skip to content

Commit a749d76

Browse files
authored
Merge pull request #137 from club-1/split-api-helper
Split APIHelper out into its own file
2 parents c6e6706 + d193708 commit a749d76

File tree

2 files changed

+288
-268
lines changed

2 files changed

+288
-268
lines changed

synadm/cli/__init__.py

+2-268
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,11 @@
1515
# You should have received a copy of the GNU General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18-
""" CLI base functions and settings
18+
""" CLI root-level commands; Subcommands are imported at the bottom of file
1919
"""
2020

21-
import os
2221
import sys
23-
import logging
24-
import pprint
25-
import json
2622
import click
27-
import yaml
28-
import tabulate
29-
from urllib.parse import urlparse
30-
import dns.resolver
31-
import re
32-
33-
from synadm import api
34-
3523

3624
output_format_help = """The 'human' mode gives a tabular or list view depending
3725
on the fetched data, but often needs a lot of horizontal space to display
@@ -42,261 +30,6 @@
4230
and is the default on fresh installations."""
4331

4432

45-
def humanize(data):
46-
""" Try to display data in a human-readable form:
47-
- Lists of dicts are displayed as tables.
48-
- Dicts are displayed as pivoted tables.
49-
- Lists are displayed as a simple list.
50-
"""
51-
if isinstance(data, list) and len(data):
52-
if isinstance(data[0], dict):
53-
headers = {header: header for header in data[0]}
54-
return tabulate.tabulate(data, tablefmt="simple", headers=headers)
55-
if isinstance(data, list):
56-
return "\n".join(data)
57-
if isinstance(data, dict):
58-
return tabulate.tabulate(data.items(), tablefmt="plain")
59-
return str(data)
60-
61-
62-
class APIHelper:
63-
""" API client enriched with CLI-level functions, used as a proxy to the
64-
client object.
65-
"""
66-
67-
FORMATTERS = {
68-
"pprint": pprint.pformat,
69-
"json": lambda data: json.dumps(data, indent=4),
70-
"minified": lambda data: json.dumps(data, separators=(",", ":")),
71-
"yaml": yaml.dump,
72-
"human": humanize
73-
}
74-
75-
CONFIG = {
76-
"user": "",
77-
"token": "",
78-
"base_url": "http://localhost:8008",
79-
"admin_path": "/_synapse/admin",
80-
"matrix_path": "/_matrix",
81-
"timeout": 30,
82-
"server_discovery": "well-known",
83-
"homeserver": "auto-retrieval",
84-
"ssl_verify": True
85-
}
86-
87-
def __init__(self, config_path, verbose, no_confirm, output_format_cli):
88-
self.config = APIHelper.CONFIG.copy()
89-
self.config_path = os.path.expanduser(config_path)
90-
self.no_confirm = no_confirm
91-
self.api = None
92-
self.init_logger(verbose)
93-
self.requests_debug = False
94-
if verbose >= 3:
95-
self.requests_debug = True
96-
self.output_format_cli = output_format_cli # override from cli
97-
98-
def init_logger(self, verbose):
99-
""" Log both to console (defaults to WARNING) and file (DEBUG).
100-
"""
101-
log_path = os.path.expanduser("~/.local/share/synadm/debug.log")
102-
os.makedirs(os.path.dirname(log_path), exist_ok=True)
103-
log = logging.getLogger("synadm")
104-
log.setLevel(logging.DEBUG)
105-
file_handler = logging.FileHandler(log_path, encoding="utf-8")
106-
file_handler.setLevel(logging.DEBUG)
107-
console_handler = logging.StreamHandler()
108-
console_handler.setLevel(
109-
logging.DEBUG if verbose > 1 else
110-
logging.INFO if verbose == 1 else
111-
logging.WARNING
112-
)
113-
file_formatter = logging.Formatter(
114-
"%(asctime)s %(name)-8s %(levelname)-7s %(message)s",
115-
datefmt="%Y-%m-%d %H:%M:%S"
116-
)
117-
console_formatter = logging.Formatter("%(levelname)-5s %(message)s")
118-
console_handler.setFormatter(console_formatter)
119-
file_handler.setFormatter(file_formatter)
120-
log.addHandler(console_handler)
121-
log.addHandler(file_handler)
122-
self.log = log
123-
124-
def _set_formatter(self, _output_format):
125-
for name, formatter in APIHelper.FORMATTERS.items():
126-
if name.startswith(_output_format):
127-
self.output_format = name
128-
self.formatter = formatter
129-
break
130-
self.log.debug("Formatter in use: %s - %s", self.output_format,
131-
self.formatter)
132-
return True
133-
134-
def load(self):
135-
""" Load the configuration and initialize the client.
136-
"""
137-
try:
138-
with open(self.config_path) as handle:
139-
self.config.update(yaml.load(handle, Loader=yaml.SafeLoader))
140-
except Exception as error:
141-
self.log.error("%s while reading configuration file", error)
142-
for key, value in self.config.items():
143-
144-
if key == "ssl_verify" and not isinstance(value, bool):
145-
self.log.error("Config value error: %s, %s must be boolean",
146-
key, value)
147-
148-
if not value and not isinstance(value, bool):
149-
self.log.error("Config entry missing: %s, %s", key, value)
150-
return False
151-
else:
152-
if key == "token":
153-
self.log.debug("Config entry read. %s: REDACTED", key)
154-
else:
155-
self.log.debug("Config entry read. %s: %s", key, value)
156-
if self.output_format_cli: # we have a cli output format override
157-
self._set_formatter(self.output_format_cli)
158-
else: # we use the configured default output format
159-
self._set_formatter(self.config["format"])
160-
self.api = api.SynapseAdmin(
161-
self.log,
162-
self.config["user"], self.config["token"],
163-
self.config["base_url"], self.config["admin_path"],
164-
self.config["timeout"], self.requests_debug,
165-
self.config["ssl_verify"]
166-
)
167-
self.matrix_api = api.Matrix(
168-
self.log,
169-
self.config["user"], self.config["token"],
170-
self.config["base_url"], self.config["matrix_path"],
171-
self.config["timeout"], self.requests_debug,
172-
self.config["ssl_verify"]
173-
)
174-
self.misc_request = api.MiscRequest(
175-
self.log,
176-
self.config["timeout"], self.requests_debug,
177-
self.config["ssl_verify"]
178-
)
179-
return True
180-
181-
def write_config(self, config):
182-
""" Write a new version of the configuration to file.
183-
"""
184-
try:
185-
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
186-
with open(self.config_path, "w") as handle:
187-
yaml.dump(config, handle, default_flow_style=False,
188-
allow_unicode=True)
189-
if os.name == "posix":
190-
click.echo("Restricting access to config file to user only.")
191-
os.chmod(self.config_path, 0o600)
192-
else:
193-
click.echo(f"Unsupported OS, please adjust permissions of "
194-
f"{self.config_path} manually")
195-
196-
return True
197-
except Exception as error:
198-
self.log.error("%s trying to write configuration", error)
199-
return False
200-
201-
def output(self, data):
202-
""" Output data object using the configured formatter.
203-
"""
204-
click.echo(self.formatter(data))
205-
206-
def retrieve_homeserver_name(self, uri=None):
207-
"""Try to retrieve the homeserver name.
208-
209-
When homeserver is set in the config already, it's just returned and
210-
nothing is tried to be fetched automatically. If not, either the
211-
location of the Federation API is looked up via a .well-known resource
212-
or a DNS SRV lookup. This depends on the server_discovery setting in
213-
the config. Finally the Federation API is used to retrieve the
214-
homeserver name.
215-
216-
Args:
217-
uri (string): proto://name:port or proto://fqdn:port
218-
219-
Returns:
220-
string: hostname, FQDN or DOMAIN; or None on errors.
221-
"""
222-
uri = uri if uri else self.config["base_url"]
223-
echo = self.log.info if self.no_confirm else click.echo
224-
if self.config["homeserver"] != "auto-retrieval":
225-
return self.config["homeserver"]
226-
227-
if self.config["server_discovery"] == "well-known":
228-
if "localhost" in self.config["base_url"]:
229-
echo(
230-
"Trying to fetch homeserver name via localhost..."
231-
)
232-
return self.matrix_api.server_name_keys_api(
233-
self.config["base_url"]
234-
)
235-
else:
236-
echo(
237-
"Trying to fetch federation URI via well-known resource..."
238-
)
239-
federation_uri = self.misc_request.federation_uri_well_known(
240-
uri
241-
)
242-
if not federation_uri:
243-
return None
244-
return self.matrix_api.server_name_keys_api(federation_uri)
245-
elif self.config["server_discovery"] == "dns":
246-
echo(
247-
"Trying to fetch federation URI via DNS SRV record..."
248-
)
249-
hostname = urlparse(uri).hostname
250-
try:
251-
record = dns.resolver.query(
252-
"_matrix._tcp.{}".format(hostname),
253-
"SRV"
254-
)
255-
except Exception as error:
256-
self.log.error(
257-
"resolving Matrix delegation for %s: %s: %s",
258-
hostname, type(error).__name__, error
259-
)
260-
else:
261-
federation_uri = "https://{}:{}".format(
262-
record[0].target, record[0].port
263-
)
264-
return self.matrix_api.server_name_keys_api(federation_uri)
265-
else:
266-
self.log.error("Unknown server_discovery mode. "
267-
"Launch synadm config!")
268-
return None
269-
270-
def generate_mxid(self, user_id):
271-
""" Checks whether the given user ID is an MXID already or else
272-
generates it from the passed string and the homeserver name fetched
273-
via the retrieve_homeserver_name method.
274-
275-
Args:
276-
user_id (string): User ID given by user as command argument.
277-
278-
Returns:
279-
string: the fully qualified Matrix User ID (MXID) or None if the
280-
user_id parameter is None or no regex matched.
281-
"""
282-
if user_id is None:
283-
self.log.debug("Missing input in generate_mxid().")
284-
return None
285-
elif re.match(r"^@[-./=\w]+:[-\[\].:\w]+$", user_id):
286-
self.log.debug("A proper MXID was passed.")
287-
return user_id
288-
elif re.match(r"^@?[-./=\w]+:?$", user_id):
289-
self.log.debug("A proper localpart was passed, generating MXID "
290-
"for local homeserver.")
291-
localpart = re.sub("[@:]", "", user_id)
292-
mxid = "@{}:{}".format(localpart, self.retrieve_homeserver_name())
293-
return mxid
294-
else:
295-
self.log.error("Neither an MXID nor a proper localpart was "
296-
"passed.")
297-
return None
298-
299-
30033
@click.group(
30134
invoke_without_command=False,
30235
context_settings=dict(help_option_names=["-h", "--help"]))
@@ -328,6 +61,7 @@ def generate_mxid(self, user_id):
32861
def root(ctx, verbose, no_confirm, output, config_file):
32962
""" the Matrix-Synapse admin CLI
33063
"""
64+
from synadm.cli._helper import APIHelper
33165
ctx.obj = APIHelper(config_file, verbose, no_confirm, output)
33266
helper_loaded = ctx.obj.load()
33367
if ctx.invoked_subcommand != "config" and not helper_loaded:

0 commit comments

Comments
 (0)