Used Maubot and Mautrix helpers for bot messages.
CI / Formatting (push) Successful in 6s
CI / Linting (push) Successful in 6s
CI / Tests (push) Successful in 29s
CI / Type Checking (push) Successful in 8s
CI / Spelling (push) Successful in 53s

This commit is contained in:
2026-05-25 21:22:28 -04:00
parent c21a8d073a
commit c0bb2cf423
5 changed files with 60 additions and 30 deletions
+16 -4
View File
@@ -127,7 +127,10 @@ class OwncastSentry(Plugin):
except Exception: except Exception:
self.metrics_service.record_error(ErrorSource.COMMAND) self.metrics_service.record_error(ErrorSource.COMMAND)
self.log.exception("Unhandled exception in subscribe command.") self.log.exception("Unhandled exception in subscribe command.")
await evt.reply("An unexpected error occurred. Please try again later.") await evt.reply(
"An unexpected error occurred. Please try again later.",
markdown=False,
)
@command.new(help="Unsubscribes from an Owncast stream.") @command.new(help="Unsubscribes from an Owncast stream.")
@command.argument("url") @command.argument("url")
@@ -138,7 +141,10 @@ class OwncastSentry(Plugin):
except Exception: except Exception:
self.metrics_service.record_error(ErrorSource.COMMAND) self.metrics_service.record_error(ErrorSource.COMMAND)
self.log.exception("Unhandled exception in unsubscribe command.") self.log.exception("Unhandled exception in unsubscribe command.")
await evt.reply("An unexpected error occurred. Please try again later.") await evt.reply(
"An unexpected error occurred. Please try again later.",
markdown=False,
)
@command.new(help="Lists all stream subscriptions in this room.") @command.new(help="Lists all stream subscriptions in this room.")
async def subscriptions(self, evt: MessageEvent) -> None: async def subscriptions(self, evt: MessageEvent) -> None:
@@ -148,7 +154,10 @@ class OwncastSentry(Plugin):
except Exception: except Exception:
self.metrics_service.record_error(ErrorSource.COMMAND) self.metrics_service.record_error(ErrorSource.COMMAND)
self.log.exception("Unhandled exception in subscriptions command.") self.log.exception("Unhandled exception in subscriptions command.")
await evt.reply("An unexpected error occurred. Please try again later.") await evt.reply(
"An unexpected error occurred. Please try again later.",
markdown=False,
)
@command.new(help="Lists currently live streams in this room.") @command.new(help="Lists currently live streams in this room.")
async def live(self, evt: MessageEvent) -> None: async def live(self, evt: MessageEvent) -> None:
@@ -158,7 +167,10 @@ class OwncastSentry(Plugin):
except Exception: except Exception:
self.metrics_service.record_error(ErrorSource.COMMAND) self.metrics_service.record_error(ErrorSource.COMMAND)
self.log.exception("Unhandled exception in live command.") self.log.exception("Unhandled exception in live command.")
await evt.reply("An unexpected error occurred. Please try again later.") await evt.reply(
"An unexpected error occurred. Please try again later.",
markdown=False,
)
async def _run_stream_updates_every_minute(self) -> None: async def _run_stream_updates_every_minute(self) -> None:
"""Run stream state updates at the top of each wall-clock minute.""" """Run stream state updates at the top of each wall-clock minute."""
+10 -5
View File
@@ -111,18 +111,21 @@ class CommandHandler:
"The URL you supplied does not appear to " "The URL you supplied does not appear to "
"be a valid Owncast instance. You may have " "be a valid Owncast instance. You may have "
"specified an invalid domain, or the " "specified an invalid domain, or the "
"instance is offline." "instance is offline.",
markdown=False,
) )
return return
except AlreadySubscribedError as e: except AlreadySubscribedError as e:
await evt.reply( await evt.reply(
f"This room is already subscribed to notifications for {e.domain}." f"This room is already subscribed to notifications for {e.domain}.",
markdown=False,
) )
return return
await evt.reply( await evt.reply(
f"Subscription added! This room will receive " f"Subscription added! This room will receive "
f"notifications when {stream_domain} goes live." f"notifications when {stream_domain} goes live.",
markdown=False,
) )
async def unsubscribe(self, evt: MessageEvent, url: str) -> None: async def unsubscribe(self, evt: MessageEvent, url: str) -> None:
@@ -137,13 +140,15 @@ class CommandHandler:
) )
except NotSubscribedError as e: except NotSubscribedError as e:
await evt.reply( await evt.reply(
f"This room is already not subscribed to notifications for {e.domain}." f"This room is already not subscribed to notifications for {e.domain}.",
markdown=False,
) )
return return
await evt.reply( await evt.reply(
f"Subscription removed! This room will no " f"Subscription removed! This room will no "
f"longer receive notifications for {stream_domain}." f"longer receive notifications for {stream_domain}.",
markdown=False,
) )
async def subscriptions(self, evt: MessageEvent) -> None: async def subscriptions(self, evt: MessageEvent) -> None:
+1 -4
View File
@@ -18,8 +18,6 @@ import asyncio
import time import time
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from mautrix.types import MessageType, TextMessageEventContent
from .metrics import NotificationType from .metrics import NotificationType
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -209,8 +207,7 @@ class NotificationService:
Propagates send errors so broadcast delivery accounting can count failures. Propagates send errors so broadcast delivery accounting can count failures.
""" """
try: try:
content = TextMessageEventContent(msgtype=MessageType.TEXT, body=body_text) await self.client.send_notice(room_id, body_text)
await self.client.send_message(room_id, content)
except Exception as exception: except Exception as exception:
self.log.warning( self.log.warning(
"[%s] Failed to send notification message to room [%s]: %s", "[%s] Failed to send notification message to room [%s]: %s",
+26 -15
View File
@@ -15,9 +15,12 @@
"""Shared test fixtures and stubs for OwncastSentry tests.""" """Shared test fixtures and stubs for OwncastSentry tests."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, override
import pytest import pytest
from maubot.matrix import MaubotMatrixClient
from maubot.testing.bot import MatrixEvent
from mautrix.types import EventContent, EventID, EventType, RoomID
from mautrix.util.async_db import Database from mautrix.util.async_db import Database
from prometheus_client import generate_latest from prometheus_client import generate_latest
@@ -185,27 +188,35 @@ VALID_STATUS_RESPONSE: dict[str, object] = {
} }
@dataclass class _StubMatrixClient(MaubotMatrixClient):
class _SentMessage: """Recording Matrix client using Mautrix message builders."""
"""A message recorded by _StubMatrixClient."""
room_id: str
content: Any
class _StubMatrixClient:
"""Recording stub for the Matrix client used in notification tests."""
def __init__(self) -> None: def __init__(self) -> None:
self.sent_messages: list[_SentMessage] = [] self.sent_messages: list[MatrixEvent] = []
self.should_fail_for_rooms: set[str] = set() self.should_fail_for_rooms: set[str] = set()
async def send_message(self, room_id: str, content: Any) -> None: @override
"""Record a sent message, or raise if room is in the fail set.""" async def send_message_event(
self,
room_id: RoomID,
event_type: EventType,
content: EventContent,
_disable_encryption: bool = False,
**kwargs: Any,
) -> EventID:
"""Record a sent message event, or raise if room is in the fail set."""
if room_id in self.should_fail_for_rooms: if room_id in self.should_fail_for_rooms:
msg = f"Stubbed failure for room {room_id}" msg = f"Stubbed failure for room {room_id}"
raise RuntimeError(msg) raise RuntimeError(msg)
self.sent_messages.append(_SentMessage(room_id=room_id, content=content)) self.sent_messages.append(
MatrixEvent(
room_id=room_id,
event_type=event_type,
content=content,
kwargs=kwargs,
)
)
return EventID("$stubbed")
@dataclass @dataclass
+7 -2
View File
@@ -20,6 +20,7 @@ import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
from mautrix.types import MessageType
from owncastsentry.metrics import MetricsService from owncastsentry.metrics import MetricsService
from owncastsentry.notification_service import ( from owncastsentry.notification_service import (
@@ -301,6 +302,7 @@ class TestNotifyStreamLive:
"#tag" "#tag"
) )
for msg in client.sent_messages: for msg in client.sent_messages:
assert msg.content.msgtype == MessageType.NOTICE
assert msg.content.body == expected_body assert msg.content.body == expected_body
async def test_records_cooldown_after_success( async def test_records_cooldown_after_success(
@@ -358,10 +360,10 @@ class TestNotifyStreamLive:
"""Count a cancelled delivery result as a failure.""" """Count a cancelled delivery result as a failure."""
client = _StubMatrixClient() client = _StubMatrixClient()
async def send_message(_room_id: str, _content: object) -> None: async def send_notice(_room_id: str, _text: str) -> None:
raise asyncio.CancelledError raise asyncio.CancelledError
client.send_message = send_message client.send_notice = send_notice
service = _make_service(client=client, subscription_repo=subscription_repo) service = _make_service(client=client, subscription_repo=subscription_repo)
await stream_repo.create("example.com") await stream_repo.create("example.com")
@@ -406,6 +408,7 @@ class TestNotifyStreamLive:
assert len(client.sent_messages) == 1 assert len(client.sent_messages) == 1
assert client.sent_messages[0].room_id == "!good:matrix.org" assert client.sent_messages[0].room_id == "!good:matrix.org"
assert client.sent_messages[0].content.msgtype == MessageType.NOTICE
assert client.sent_messages[0].content.body == ( assert client.sent_messages[0].content.body == (
"🎥 Stream is now live!\n" "🎥 Stream is now live!\n"
"Stream Title: Title\n" "Stream Title: Title\n"
@@ -432,6 +435,7 @@ class TestSendCleanupWarning:
await service.send_cleanup_warning("example.com") await service.send_cleanup_warning("example.com")
assert len(client.sent_messages) == 1 assert len(client.sent_messages) == 1
assert client.sent_messages[0].content.msgtype == MessageType.NOTICE
assert client.sent_messages[0].content.body == ( assert client.sent_messages[0].content.body == (
"⚠️ Warning: Subscription Cleanup Scheduled\n" "⚠️ Warning: Subscription Cleanup Scheduled\n"
"\n" "\n"
@@ -461,6 +465,7 @@ class TestSendCleanupDeletion:
await service.send_cleanup_deletion("example.com") await service.send_cleanup_deletion("example.com")
assert len(client.sent_messages) == 1 assert len(client.sent_messages) == 1
assert client.sent_messages[0].content.msgtype == MessageType.NOTICE
assert client.sent_messages[0].content.body == ( assert client.sent_messages[0].content.body == (
"🗑️ Subscription Automatically Removed\n" "🗑️ Subscription Automatically Removed\n"
"\n" "\n"