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 nio
import yaml 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. # Not going to care for specifics like the underscore.
# Generally match !anything:example.com with unicode support. # 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) room_id = await resolve_room(client=client, room=args.channel)
response = await client.join(room_id=room_id) response = await client.join(room_id=room_id)
if isinstance(response, nio.ErrorResponse): if isinstance(response, nio.ErrorResponse):
raise response raise MatrixException(response)
if "html" in args: 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: 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) print("Message sent.", file=sys.stderr, flush=True)
finally: finally:
await client.close() await client.close()

165
wmn.py
View File

@ -25,13 +25,20 @@ from datetime import datetime
from typing import Tuple, Optional, Dict, Any from typing import Tuple, Optional, Dict, Any
import nio import nio
import yaml
from flask import Flask, request, abort from flask import Flask, request, abort
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
import dateutil.parser import dateutil.parser
Cfg = Dict[str, Any] from common import (
ErrorResponse = Tuple[str, int] client_login,
send_message,
Cfg,
resolve_room,
format_response,
load_configuration,
MatrixException,
)
RequestArgs = MultiDict[str, str] RequestArgs = MultiDict[str, str]
app = Flask(__name__) app = Flask(__name__)
@ -42,16 +49,6 @@ application = app
room_pattern = re.compile(r"^[!#]\w+:[\w\-.]+$") 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): def check_token(configuration: Cfg, token: str):
if token != configuration["secret"]: if token != configuration["secret"]:
print( print(
@ -62,20 +59,6 @@ def check_token(configuration: Cfg, token: str):
abort(401) 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: 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.""" """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) 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): def get_msg_type(request_args: RequestArgs):
@ -142,62 +128,6 @@ def shorten(string: str, max_len: int = 80, appendix: str = "..."):
return string 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(): async def process_gitlab_request():
cfg = load_configuration() cfg = load_configuration()
check_token(configuration=cfg, token=request.headers.get("X-Gitlab-Token")) check_token(configuration=cfg, token=request.headers.get("X-Gitlab-Token"))
@ -205,8 +135,8 @@ async def process_gitlab_request():
try: try:
client = await client_login(cfg) client = await client_login(cfg)
except nio.ErrorResponse as response: except MatrixException as error:
return matrix_error(response) return error.format_response()
try: try:
room_id = await get_a_room(client, request.args) 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) response = await client.join(room_id=room_id)
if isinstance(response, nio.ErrorResponse): if isinstance(response, nio.ErrorResponse):
return matrix_error(response) return format_response(response)
def sort_commits_by_time(commits): def sort_commits_by_time(commits):
return sorted(commits, key=lambda commit: commit["timestamp"]) 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"])) map(extract_commit_info, sort_commits_by_time(request.json["commits"]))
) )
html_commits = "\n".join( 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( text_commits = "\n".join(
(f"- [{msg}]({url})" for (msg, url) in commit_messages) (f"- [{msg}]({url})" for (msg, url) in commit_messages)
@ -263,10 +196,10 @@ async def process_gitlab_request():
ignore_unverified_devices=True, ignore_unverified_devices=True,
) )
if isinstance(response, nio.ErrorResponse): if isinstance(response, nio.ErrorResponse):
return matrix_error(response) return format_response(response)
except nio.ErrorResponse as response: except MatrixException as error:
abort(app.make_response(matrix_error(response))) abort(app.make_response(error.format_response()))
finally: finally:
await client.close() await client.close()
@ -281,8 +214,8 @@ async def process_jenkins_request():
try: try:
client = await client_login(cfg) client = await client_login(cfg)
except nio.ErrorResponse as response: except MatrixException as error:
return matrix_error(response) return error.format_response()
try: try:
room_id = await get_a_room(client, request.args) room_id = await get_a_room(client, request.args)
@ -297,7 +230,9 @@ async def process_jenkins_request():
htimestamp = datetime.fromtimestamp( htimestamp = datetime.fromtimestamp(
change["timestamp"] / 1000 change["timestamp"] / 1000
).strftime("%d. %b %y %H:%M") ).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: if project_url is not None and project_url:
commit_link = f"<a href=\"{project_url}commit/{change['commitId']}\">{bare_commit_link}</a>" commit_link = f"<a href=\"{project_url}commit/{change['commitId']}\">{bare_commit_link}</a>"
else: else:
@ -350,8 +285,8 @@ async def process_jenkins_request():
), ),
) )
except nio.ErrorResponse as response: except MatrixException as error:
abort(app.make_response(matrix_error(response))) abort(app.make_response(error.format_response()))
finally: finally:
await client.close() await client.close()
@ -415,12 +350,16 @@ async def process_prometheus_request():
alert_daterange = [] alert_daterange = []
if "startsAt" in alert and alert["startsAt"] != "0001-01-01T00:00:00Z": if "startsAt" in alert and alert["startsAt"] != "0001-01-01T00:00:00Z":
alert_start = ( 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}") alert_daterange.append(f"started at {alert_start}")
if "endsAt" in alert and alert["endsAt"] != "0001-01-01T00:00:00Z": if "endsAt" in alert and alert["endsAt"] != "0001-01-01T00:00:00Z":
alert_end = ( 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.append(f"ended at {alert_end}")
alert_daterange = ", ".join(alert_daterange) alert_daterange = ", ".join(alert_daterange)
@ -457,8 +396,8 @@ async def process_prometheus_request():
try: try:
client = await client_login(cfg) client = await client_login(cfg)
except nio.ErrorResponse as response: except MatrixException as error:
return matrix_error(response) return error.format_response()
try: try:
msgtype = get_msg_type(request_args=request.args) msgtype = get_msg_type(request_args=request.args)
@ -468,19 +407,33 @@ async def process_prometheus_request():
abort(400) abort(400)
try: 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: 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: 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): 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) print("Error parsing JSON and forming message:", file=sys.stderr)
traceback.print_exc() traceback.print_exc()
print(file=sys.stderr, flush=True) print(file=sys.stderr, flush=True)
return "Error parsing JSON and forming message", 500 return "Error parsing JSON and forming message", 500
except nio.ErrorResponse as response: except MatrixException as error:
abort(app.make_response(matrix_error(response))) abort(app.make_response(error.format_response()))
finally: finally:
await client.close() await client.close()