Skip to content

Commit

Permalink
feat: implement row output
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelSasser committed Dec 17, 2024
1 parent 54dab05 commit 7588a43
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 37 deletions.
43 changes: 43 additions & 0 deletions src/matrixctl/argparse_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Custom argparse action classes."""

from __future__ import annotations

import typing as t

from argparse import Action
from enum import Enum


# https://docs.python.org/3/library/argparse.html#action-classes
class ArgparseActionEnum(Action):
"""Custom argparse action for Enums."""

def __init__(
self,
choices: t.Sequence[str] | None = None,
type: t.Any | None = None, # noqa: A002
**kwargs, # noqa: ANN003
) -> None:
enum: t.Any | None = type
if enum is None:
err_msg = "The enum must be provided as argument 'type'."
raise TypeError(err_msg)
if not issubclass(enum, Enum):
err_msg = f"The argument type must be an Enum. The type was {enum}"
raise TypeError(err_msg)

self._enum = enum

choices = choices or tuple(enum_type.value for enum_type in self._enum)

super().__init__(choices=choices, **kwargs)

def __call__( # noqa: ANN204, D102
self,
_parser,
namespace: t.Any,
values: str | t.Sequence[t.Any],
_option_string: str | None = None,
):
value = self._enum(values)
setattr(namespace, self.dest, value)
281 changes: 259 additions & 22 deletions src/matrixctl/commands/get_events/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@

from __future__ import annotations

import datetime
import json
import logging
import typing as t

from argparse import Namespace

from rich.console import Console
from rich.highlighter import RegexHighlighter
from rich.text import Text
from rich.theme import Theme

from .parser import OutputType

from matrixctl.handlers.db import db_connect
from matrixctl.handlers.yaml import YAML
from matrixctl.sanitizers import MessageType
Expand All @@ -39,6 +47,167 @@
logger = logging.getLogger(__name__)


class HighlighterMatrixUser(RegexHighlighter):
"""Apply style to anything that looks like an email."""

base_style = "example."
highlights = [r"(?P<matrix_user>@[\w-]+:([\w-]+\.)+[\w-]+)"]


def to_row_ctx(kind: MessageType | str, ev: dict[str, t.Any]) -> Text:
"""Create an event context from a message type and it's content."""

content = ev.get("content")
ctx = Text()
if isinstance(kind, str) or content is None:
ctx.append("Unknown Message Type")

# 2024-11-12T20:33:13 | !iuyQXswfjgxQMZGrfQ:matrix.org | @michael:michaelsasser.org | m.room.redaction | $zVjqu0tytOH5jeyfHYbTXAlnQRkYCV64jqm73pYzhgw | Unknown Message Type

elif kind == MessageType.M_ROOM_REDACTION:
redacts = ev.get("redacts")

ctx.append("REDACTION ", "bright_black italic")
if redacts is not None:
ctx.append(f"{{ redacts={redacts} }}", style="bright_black")
elif kind == MessageType.M_ROOM_GUEST_ACCESS:
ctx.append("GUEST ACCESS ", "bright_black italic")
ctx.append(f"{{ content={content} }}", style="bright_black")
elif kind == MessageType.M_ROOM_HISTORY_VISIBILITY:
ctx.append("HISTORY VISIBILITY ", "bright_black italic")
ctx.append(f"{{ content={content} }}", style="bright_black")
elif kind == MessageType.M_ROOM_JOIN_RULES:
ctx.append("JOIN RULES ", "bright_black italic")
ctx.append(f"{{ content={content} }}", style="bright_black")
elif kind == MessageType.M_ROOM_POWER_LEVELS:
ctx.append("POWER LEVELS ", "bright_black italic")
ctx.append(f"{{ content={content} }}", style="bright_black")

elif kind == MessageType.M_ROOM_ENCRYPTED:
ctx.append("MESSAGE ENCRYPTED", "bright_black italic")
elif kind == MessageType.M_REACTION:
relates_to = content.get("m.relates_to")

if relates_to is not None:
relates_to_event_id = relates_to.get("event_id")
rel_type = relates_to.get("rel_type")

if rel_type == "m.annotation":
ctx.append("ANNOTATION ", "bright_black italic")
ctx.append("{ ", style="bright_black")

key = relates_to.get("key")

ctx.append(
"key='",
"bright_black",
)

ctx.append(
key,
"green",
)

ctx.append(
"' ",
"bright_black",
)

ctx.append(
f"relates_to={relates_to_event_id} rel_type={rel_type} ",
"bright_black",
)

ctx.append("}", style="bright_black")
elif kind == MessageType.M_ROOM_MESSAGE:
mgstype: str = str(content.get("msgtype"))
body: str = str(content.get("body"))

if mgstype in {"m.text", "m.notice"}:
ctx.append(
f"{mgstype.lstrip('m.').upper() } ", "bright_black italic"
)
ctx.append("{ ", style="bright_black")

ctx.append("body='", style="bright_black")
ctx.append(body, style="green")
ctx.append("' ", style="bright_black")

relates_to = content.get("m.relates_to")
reply_to_event_id = None
if relates_to is not None:
in_reply_to = relates_to.get("m.in_reply_to")
if in_reply_to is not None:
reply_to_event_id = in_reply_to.get("event_id")
ctx.append(
f"replies_to={reply_to_event_id} ", "bright_black"
)

ctx.append("}", style="bright_black")
elif mgstype == "m.image":
url = content.get("url")

info = content.get("info")

ctx.append("IMAGE ", "bright_black italic")
ctx.append(
f"{{ {body=}, {url=}",
"bright_black",
)
if info is not None:
mimetype = info.get("mimetype")
size = info.get("size")
width = info.get("w")
height = info.get("h")

ctx.append(
(
f" mimetype={mimetype}, size={size},"
f" width={width}, height={height}"
),
"bright_black",
)
ctx.append(" }", "bright_black")
elif mgstype == "m.file":
url = content.get("url")

info = content.get("info")

ctx.append("FILE ", "bright_black italic")
ctx.append(
f"{{ {body=}, {url=}",
"bright_black",
)
if info is not None:
mimetype = info.get("mimetype")
size = info.get("size")

ctx.append(
(f" mimetype={mimetype}, size={size},"),
"bright_black",
)
ctx.append(" }", "bright_black")

else:
ctx.append(f"Unknown Message Type '{mgstype}")

elif kind == MessageType.M_ROOM_MEMBER:
avatar_url: str | None = content.get("avatar_url")
displayname: str | None = content.get("displayname")
membership: str | None = content.get("membership")

ctx.append("MEMBERSHIP ", "bright_black italic")
ctx.append(
f"{{ {membership=}, {displayname=}, {avatar_url=} }}",
"bright_black",
)

else:
ctx.append("Unknown Message Type")

return ctx


def addon(arg: Namespace, yaml: YAML) -> int:
"""Get Events from the Server.
Expand Down Expand Up @@ -74,15 +243,12 @@ def addon(arg: Namespace, yaml: YAML) -> int:

# Sanitize message_type
message_type: MessageType | t.Literal[False] | None = (
sanitize_message_type(arg.type)
sanitize_message_type(arg.event_type)
)
if message_type is False:
return 1

query: str = (
"SELECT json FROM event_json WHERE event_id IN ("
"SELECT event_id FROM events WHERE sender = (%s)"
)
query: str = "WITH evs AS (SELECT event_id, origin_server_ts, received_ts FROM events WHERE sender = (%s)"
values = [user_identifier]

# Add room identifier to the query
Expand All @@ -97,26 +263,97 @@ def addon(arg: Namespace, yaml: YAML) -> int:

query += ")"

query += "SELECT event_json.event_id, event_json.json, evs.origin_server_ts, evs.received_ts FROM event_json INNER JOIN evs ON event_json.event_id = evs.event_id ORDER BY evs.origin_server_ts ASC"

with db_connect(yaml) as conn, conn.cursor() as cur:
cur.execute(query, values)
try:
print("[", end="")
not_first_line: bool = False
for event in cur:
if not_first_line:
print(",")
else:
not_first_line = True
print(
json.dumps(json.loads(event[0]), indent=4),
end="",
if arg.output_format == OutputType.ROWS:
try:
for event in cur:
event_id = event[0]
ev = json.loads(event[1])

origin_server_ts_ = int(event[2])
received_ts_ = int(event[3])

origin_server_ts = datetime.datetime.fromtimestamp(
origin_server_ts_ / 1000.0, tz=datetime.timezone.utc
)
received_ts = datetime.datetime.fromtimestamp(
received_ts_ / 1000.0, tz=datetime.timezone.utc
)

tdelta: datetime.timedelta = received_ts.replace(
microsecond=0
) - origin_server_ts.replace(microsecond=0)

ts_str = (
origin_server_ts.replace(microsecond=0)
.isoformat()
.replace("+00:00", "")
)
room_id = ev.get("room_id")

sender = ev.get("sender")

kind_: str = ev.get("type")
kind: MessageType | str
try:
kind = MessageType.from_string(kind_)
except ValueError:
kind = kind_

# content = ev.get("content")

ctx = to_row_ctx(kind, ev)

theme = Theme({"example.matrix_user": "magenta bold"})
console = Console(
highlighter=HighlighterMatrixUser(), theme=theme
)
text = Text()
text.append(ts_str, style="blue bold")
text.append(" | ", style="bright_black")
text.append(room_id, style="bright_yellow")
text.append(" | ", style="bright_black")
text.append(sender, style="bright_magenta")
text.append(" | ", style="bright_black")
text.append(kind_, style="steel_blue1")
text.append(" | ", style="bright_black")
text.append(event_id, style="purple3")
text.append(" | ", style="bright_black")
text.append_text(ctx)
if tdelta.total_seconds() > 30.0:
text.append(" | ", style="bright_black")
text.append(f"Δt = {tdelta}", style="red bold")
console.print(text, soft_wrap=True)

print()

except json.decoder.JSONDecodeError:
logger.exception(
"Unable to process the response data to JSON.",
)
print("]")
except json.decoder.JSONDecodeError:
logger.exception(
"Unable to process the response data to JSON.",
)
return 1
return 1
if arg.output_format == OutputType.JSON:
try:
print("[", end="")
not_first_line: bool = False
for event in cur:
if not_first_line:
print(",")
else:
not_first_line = True
print(
json.dumps(json.loads(event[1]), indent=4),
end="",
)
print("]")
except json.decoder.JSONDecodeError:
logger.exception(
"Unable to process the response data to JSON.",
)
return 1
return 0


Expand Down
Loading

0 comments on commit 7588a43

Please sign in to comment.