Add exception wrapping nio.ErrorResponse

Fixes wrong usage nio.ErrorResponse as an exception.
This commit is contained in:
Ben 2021-10-28 20:48:06 +02:00
parent 3dd697755d
commit 56a5ce3cef
Signed by: ben
GPG key ID: 0F54A7ED232D3319
3 changed files with 215 additions and 110 deletions

144
common.py Normal file
View file

@ -0,0 +1,144 @@
# Copyright 2021 Benedikt Ziemons
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
from typing import Optional, Dict, Any, Tuple
import nio
import yaml
Cfg = Dict[str, Any]
ErrorResponseTuple = Tuple[str, int]
def format_response(error: "nio.ErrorResponse") -> ErrorResponseTuple:
"""
:returns: tuple to be interpreted as (body, status), see Flask.make_response
:rtype: ErrorResponseTuple
"""
print("matrix_error was called with", error, file=sys.stderr, flush=True)
if error.status_code:
status = int(error.status_code)
else:
status = 500
return f"Error from Matrix: {error.message}", status
class MatrixException(Exception):
def __init__(self, response: "nio.ErrorResponse"):
super(MatrixException, self).__init__("Error from Matrix: " + response.message)
self.response = response
def format_response(self) -> ErrorResponseTuple:
return format_response(self.response)
def load_configuration() -> Cfg:
with open("config.yml", "r") as ymlfile:
return yaml.safe_load(ymlfile)
def save_configuration(configuration: Cfg):
with open("config.yml", "w") as ymlfile:
yaml.safe_dump(configuration, ymlfile)
async def client_login(configuration: Cfg) -> nio.AsyncClient:
"""
:exception MatrixException: if the matrix server returns an error.
:param configuration: the configuration object to load login data from.
:type configuration: Cfg
:return: the matrix client.
:rtype: nio.AsyncClient
"""
client = nio.AsyncClient(
homeserver=configuration["matrix"].get("server"),
user=configuration["matrix"].get("username", ""),
device_id=configuration["matrix"].get("device_id", ""),
store_path=configuration["matrix"].get("store_path", ""),
)
response = await client.login(
password=configuration["matrix"].get("password", None),
device_name=configuration["matrix"].get("device_name", ""),
token=configuration["matrix"].get("token", None),
)
if isinstance(response, nio.ErrorResponse):
raise MatrixException(response)
if "device_id" not in configuration["matrix"]:
configuration["matrix"]["device_id"] = response.device_id
save_configuration(configuration)
return client
async def send_message(
client: nio.AsyncClient,
room_id: str,
text: str,
msgtype: str = "m.text",
html: Optional[str] = None,
) -> nio.RoomSendResponse:
"""
:exception MatrixException: if the matrix server returns an error.
:param client: the client to operate on.
:param room_id: the room to send the message to.
:param text: the text to send.
:param msgtype: the message type to use. By default this is "m.text".
:param html: optional html string to send with the message.
:return: a room send response, which is never an nio.ErrorResponse.
:rtype: nio.RoomSendResponse
"""
content = {
"body": text,
"msgtype": msgtype,
}
if html is not None:
content["format"] = "org.matrix.custom.html"
content["formatted_body"] = html
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content=content,
ignore_unverified_devices=True,
)
if isinstance(response, nio.ErrorResponse):
raise MatrixException(response)
return response
async def resolve_room(client: nio.AsyncClient, room: str) -> str:
"""
Takes a room alias or room id and always returns a resolved room id.
:exception MatrixException: if the matrix server returns an error.
:exception RuntimeError: if the passed room string cannot be handled.
:param client: the client to operate on.
:param room: the room to resolve.
:returns: the room's matrix id, starting with a "!".
:rtype: str
"""
if room.startswith("#"):
response = await client.room_resolve_alias(room_alias=room)
if isinstance(response, nio.ErrorResponse):
raise MatrixException(response)
return response.room_id
elif room.startswith("!"):
return room
else:
raise RuntimeError(f"Room {room} could not be resolved")

View file

@ -26,7 +26,7 @@ import sys
import nio
import yaml
from wmn import client_login, resolve_room, send_message
from common import client_login, send_message, resolve_room, MatrixException
# Not going to care for specifics like the underscore.
# Generally match !anything:example.com with unicode support.
@ -72,12 +72,20 @@ async def main():
room_id = await resolve_room(client=client, room=args.channel)
response = await client.join(room_id=room_id)
if isinstance(response, nio.ErrorResponse):
raise response
raise MatrixException(response)
if "html" in args:
response = await send_message(client=client, room_id=room_id, text=(args.text or ""), msgtype=args.type, html=args.html)
response = await send_message(
client=client,
room_id=room_id,
text=(args.text or ""),
msgtype=args.type,
html=args.html,
)
else:
response = await send_message(client=client, room_id=room_id, text=args.text, msgtype=args.type)
response = await send_message(
client=client, room_id=room_id, text=args.text, msgtype=args.type
)
print("Message sent.", file=sys.stderr, flush=True)
finally:
await client.close()

165
wmn.py
View file

@ -25,13 +25,20 @@ from datetime import datetime
from typing import Tuple, Optional, Dict, Any
import nio
import yaml
from flask import Flask, request, abort
from werkzeug.datastructures import MultiDict
import dateutil.parser
Cfg = Dict[str, Any]
ErrorResponse = Tuple[str, int]
from common import (
client_login,
send_message,
Cfg,
resolve_room,
format_response,
load_configuration,
MatrixException,
)
RequestArgs = MultiDict[str, str]
app = Flask(__name__)
@ -42,16 +49,6 @@ application = app
room_pattern = re.compile(r"^[!#]\w+:[\w\-.]+$")
def load_configuration() -> Cfg:
with open("config.yml", "r") as ymlfile:
return yaml.safe_load(ymlfile)
def save_configuration(configuration: Cfg):
with open("config.yml", "w") as ymlfile:
yaml.safe_dump(configuration, ymlfile)
def check_token(configuration: Cfg, token: str):
if token != configuration["secret"]:
print(
@ -62,20 +59,6 @@ def check_token(configuration: Cfg, token: str):
abort(401)
async def resolve_room(client: nio.AsyncClient, room: str) -> str:
"""Takes a room alias or room id and always returns a resolved room id."""
if room.startswith("#"):
response = await client.room_resolve_alias(room_alias=room)
if isinstance(response, nio.ErrorResponse):
abort(app.make_response(matrix_error(response)))
return response.room_id
elif room.startswith("!"):
return room
else:
raise RuntimeError(f"Room {room} could not be resolved")
async def get_a_room(client: nio.AsyncClient, request_args: RequestArgs) -> str:
"""Takes a nio.AsyncClient and the request args to return a room id."""
@ -107,7 +90,10 @@ async def get_a_room(client: nio.AsyncClient, request_args: RequestArgs) -> str:
)
abort(400)
return await resolve_room(client=client, room=room)
try:
return await resolve_room(client=client, room=room)
except MatrixException as error:
abort(app.make_response(error.format_response()))
def get_msg_type(request_args: RequestArgs):
@ -142,62 +128,6 @@ def shorten(string: str, max_len: int = 80, appendix: str = "..."):
return string
async def client_login(configuration: Cfg) -> nio.AsyncClient:
client = nio.AsyncClient(
homeserver=configuration["matrix"].get("server"),
user=configuration["matrix"].get("username", ""),
device_id=configuration["matrix"].get("device_id", ""),
store_path=configuration["matrix"].get("store_path", ""),
)
response = await client.login(
password=configuration["matrix"].get("password", None),
device_name=configuration["matrix"].get("device_name", ""),
token=configuration["matrix"].get("token", None),
)
if isinstance(response, nio.ErrorResponse):
raise response
if "device_id" not in configuration["matrix"]:
configuration["matrix"]["device_id"] = response.device_id
save_configuration(configuration)
return client
async def send_message(
client: nio.AsyncClient,
room_id: str,
text: str,
msgtype: str = "m.text",
html: Optional[str] = None
) -> nio.RoomSendResponse:
content = {
"body": text,
"msgtype": msgtype,
}
if html is not None:
content["format"] = "org.matrix.custom.html"
content["formatted_body"] = html
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content=content,
ignore_unverified_devices=True,
)
if isinstance(response, nio.ErrorResponse):
raise response
return response
def matrix_error(error: nio.ErrorResponse) -> ErrorResponse:
print("matrix_error was called with", error, file=sys.stderr, flush=True)
# see Flask.make_response, this will be interpreted as (body, status)
if error.status_code:
status = int(error.status_code)
else:
status = 500
return f"Error from Matrix: {error.message}", status
async def process_gitlab_request():
cfg = load_configuration()
check_token(configuration=cfg, token=request.headers.get("X-Gitlab-Token"))
@ -205,8 +135,8 @@ async def process_gitlab_request():
try:
client = await client_login(cfg)
except nio.ErrorResponse as response:
return matrix_error(response)
except MatrixException as error:
return error.format_response()
try:
room_id = await get_a_room(client, request.args)
@ -218,7 +148,7 @@ async def process_gitlab_request():
response = await client.join(room_id=room_id)
if isinstance(response, nio.ErrorResponse):
return matrix_error(response)
return format_response(response)
def sort_commits_by_time(commits):
return sorted(commits, key=lambda commit: commit["timestamp"])
@ -244,7 +174,10 @@ async def process_gitlab_request():
map(extract_commit_info, sort_commits_by_time(request.json["commits"]))
)
html_commits = "\n".join(
(f' <li><a href="{url}">{msg}</a></li>' for (msg, url) in commit_messages)
(
f' <li><a href="{url}">{msg}</a></li>'
for (msg, url) in commit_messages
)
)
text_commits = "\n".join(
(f"- [{msg}]({url})" for (msg, url) in commit_messages)
@ -263,10 +196,10 @@ async def process_gitlab_request():
ignore_unverified_devices=True,
)
if isinstance(response, nio.ErrorResponse):
return matrix_error(response)
return format_response(response)
except nio.ErrorResponse as response:
abort(app.make_response(matrix_error(response)))
except MatrixException as error:
abort(app.make_response(error.format_response()))
finally:
await client.close()
@ -281,8 +214,8 @@ async def process_jenkins_request():
try:
client = await client_login(cfg)
except nio.ErrorResponse as response:
return matrix_error(response)
except MatrixException as error:
return error.format_response()
try:
room_id = await get_a_room(client, request.args)
@ -297,7 +230,9 @@ async def process_jenkins_request():
htimestamp = datetime.fromtimestamp(
change["timestamp"] / 1000
).strftime("%d. %b %y %H:%M")
bare_commit_link = f"({shorten(change['commitId'], 7, appendix='')})"
bare_commit_link = (
f"({shorten(change['commitId'], 7, appendix='')})"
)
if project_url is not None and project_url:
commit_link = f"<a href=\"{project_url}commit/{change['commitId']}\">{bare_commit_link}</a>"
else:
@ -350,8 +285,8 @@ async def process_jenkins_request():
),
)
except nio.ErrorResponse as response:
abort(app.make_response(matrix_error(response)))
except MatrixException as error:
abort(app.make_response(error.format_response()))
finally:
await client.close()
@ -415,12 +350,16 @@ async def process_prometheus_request():
alert_daterange = []
if "startsAt" in alert and alert["startsAt"] != "0001-01-01T00:00:00Z":
alert_start = (
dateutil.parser.isoparse(alert["startsAt"]).strftime("%d. %b %y %H:%M %Z").rstrip()
dateutil.parser.isoparse(alert["startsAt"])
.strftime("%d. %b %y %H:%M %Z")
.rstrip()
)
alert_daterange.append(f"started at {alert_start}")
if "endsAt" in alert and alert["endsAt"] != "0001-01-01T00:00:00Z":
alert_end = (
dateutil.parser.isoparse(alert["endsAt"]).strftime("%d. %b %y %H:%M %Z").rstrip()
dateutil.parser.isoparse(alert["endsAt"])
.strftime("%d. %b %y %H:%M %Z")
.rstrip()
)
alert_daterange.append(f"ended at {alert_end}")
alert_daterange = ", ".join(alert_daterange)
@ -457,8 +396,8 @@ async def process_prometheus_request():
try:
client = await client_login(cfg)
except nio.ErrorResponse as response:
return matrix_error(response)
except MatrixException as error:
return error.format_response()
try:
msgtype = get_msg_type(request_args=request.args)
@ -468,19 +407,33 @@ async def process_prometheus_request():
abort(400)
try:
for text, html in map(extract_alert_message, request.json.get("alerts", [])):
for text, html in map(
extract_alert_message, request.json.get("alerts", [])
):
if html and text:
await send_message(client=client, room_id=room_id, text=text, msgtype=msgtype, html=html)
await send_message(
client=client,
room_id=room_id,
text=text,
msgtype=msgtype,
html=html,
)
elif text:
await send_message(client=client, room_id=room_id, text=text, msgtype=msgtype)
await send_message(
client=client, room_id=room_id, text=text, msgtype=msgtype
)
except (LookupError, ValueError, TypeError):
await send_message(client=client, room_id=room_id, text="Error parsing data in prometheus request")
await send_message(
client=client,
room_id=room_id,
text="Error parsing data in prometheus request",
)
print("Error parsing JSON and forming message:", file=sys.stderr)
traceback.print_exc()
print(file=sys.stderr, flush=True)
return "Error parsing JSON and forming message", 500
except nio.ErrorResponse as response:
abort(app.make_response(matrix_error(response)))
except MatrixException as error:
abort(app.make_response(error.format_response()))
finally:
await client.close()