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:
self.metrics_service.record_error(ErrorSource.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.argument("url")
@@ -138,7 +141,10 @@ class OwncastSentry(Plugin):
except Exception:
self.metrics_service.record_error(ErrorSource.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.")
async def subscriptions(self, evt: MessageEvent) -> None:
@@ -148,7 +154,10 @@ class OwncastSentry(Plugin):
except Exception:
self.metrics_service.record_error(ErrorSource.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.")
async def live(self, evt: MessageEvent) -> None:
@@ -158,7 +167,10 @@ class OwncastSentry(Plugin):
except Exception:
self.metrics_service.record_error(ErrorSource.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:
"""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 "
"be a valid Owncast instance. You may have "
"specified an invalid domain, or the "
"instance is offline."
"instance is offline.",
markdown=False,
)
return
except AlreadySubscribedError as e:
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
await evt.reply(
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:
@@ -137,13 +140,15 @@ class CommandHandler:
)
except NotSubscribedError as e:
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
await evt.reply(
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:
+1 -4
View File
@@ -18,8 +18,6 @@ import asyncio
import time
from typing import TYPE_CHECKING, Any
from mautrix.types import MessageType, TextMessageEventContent
from .metrics import NotificationType
if TYPE_CHECKING:
@@ -209,8 +207,7 @@ class NotificationService:
Propagates send errors so broadcast delivery accounting can count failures.
"""
try:
content = TextMessageEventContent(msgtype=MessageType.TEXT, body=body_text)
await self.client.send_message(room_id, content)
await self.client.send_notice(room_id, body_text)
except Exception as exception:
self.log.warning(
"[%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."""
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, override
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 prometheus_client import generate_latest
@@ -185,27 +188,35 @@ VALID_STATUS_RESPONSE: dict[str, object] = {
}
@dataclass
class _SentMessage:
"""A message recorded by _StubMatrixClient."""
room_id: str
content: Any
class _StubMatrixClient:
"""Recording stub for the Matrix client used in notification tests."""
class _StubMatrixClient(MaubotMatrixClient):
"""Recording Matrix client using Mautrix message builders."""
def __init__(self) -> None:
self.sent_messages: list[_SentMessage] = []
self.sent_messages: list[MatrixEvent] = []
self.should_fail_for_rooms: set[str] = set()
async def send_message(self, room_id: str, content: Any) -> None:
"""Record a sent message, or raise if room is in the fail set."""
@override
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:
msg = f"Stubbed failure for room {room_id}"
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
+7 -2
View File
@@ -20,6 +20,7 @@ import time
from typing import TYPE_CHECKING
import pytest
from mautrix.types import MessageType
from owncastsentry.metrics import MetricsService
from owncastsentry.notification_service import (
@@ -301,6 +302,7 @@ class TestNotifyStreamLive:
"#tag"
)
for msg in client.sent_messages:
assert msg.content.msgtype == MessageType.NOTICE
assert msg.content.body == expected_body
async def test_records_cooldown_after_success(
@@ -358,10 +360,10 @@ class TestNotifyStreamLive:
"""Count a cancelled delivery result as a failure."""
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
client.send_message = send_message
client.send_notice = send_notice
service = _make_service(client=client, subscription_repo=subscription_repo)
await stream_repo.create("example.com")
@@ -406,6 +408,7 @@ class TestNotifyStreamLive:
assert len(client.sent_messages) == 1
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 == (
"🎥 Stream is now live!\n"
"Stream Title: Title\n"
@@ -432,6 +435,7 @@ class TestSendCleanupWarning:
await service.send_cleanup_warning("example.com")
assert len(client.sent_messages) == 1
assert client.sent_messages[0].content.msgtype == MessageType.NOTICE
assert client.sent_messages[0].content.body == (
"⚠️ Warning: Subscription Cleanup Scheduled\n"
"\n"
@@ -461,6 +465,7 @@ class TestSendCleanupDeletion:
await service.send_cleanup_deletion("example.com")
assert len(client.sent_messages) == 1
assert client.sent_messages[0].content.msgtype == MessageType.NOTICE
assert client.sent_messages[0].content.body == (
"🗑️ Subscription Automatically Removed\n"
"\n"