Added rate limiting to notifications to limit them to once per 20 minutes per stream.
All checks were successful
Build Maubot Plugin Artifact / Build (push) Successful in 3s
Lint Source Code / Lint (push) Successful in 8s

There's also a small amount of preliminary work included in this commit for a new type of notification when streams change their title in the middle of a session.
This commit is contained in:
2025-04-12 15:43:05 -04:00
parent b0868c5bd4
commit ee61ea8562

View File

@@ -8,6 +8,7 @@ import sqlite3
import aiohttp import aiohttp
import json import json
import asyncio import asyncio
import time
from maubot import Plugin, MessageEvent from maubot import Plugin, MessageEvent
from maubot.handlers import command from maubot.handlers import command
@@ -28,6 +29,16 @@ USER_AGENT = (
"OwncastSentry/1.0.3 (bot; +https://git.logal.dev/LogalDeveloper/OwncastSentry)" "OwncastSentry/1.0.3 (bot; +https://git.logal.dev/LogalDeveloper/OwncastSentry)"
) )
# Hard minimum amount of time between when notifications can be sent for a stream. Prevents spamming notifications for glitchy or malicious streams.
SECONDS_BETWEEN_NOTIFICATIONS = 20 * 60 # 20 minutes in seconds
# I'm not sure the best way to name or explain this variable, so let's just say what uses it:
#
# After a stream goes offline, a timer is started. Then, ...
# - If a stream comes back online with the same title within this time, no notification is sent.
# - If a stream comes back online with a different title, a rename notification is sent.
# - If this time period passes entirely and a stream comes back online after, it's treated as regular going live.
TEMPORARY_OFFLINE_NOTIFICATION_COOLDOWN = 7 * 60 # 7 minutes in seconds
# ===== DATABASE MIGRATIONS ===== # ===== DATABASE MIGRATIONS =====
upgrade_table = UpgradeTable() upgrade_table = UpgradeTable()
@@ -81,6 +92,12 @@ class OwncastSentry(Plugin):
headers=headers, cookie_jar=cookie_jar, timeout=timeouts, connector=baseconnctor headers=headers, cookie_jar=cookie_jar, timeout=timeouts, connector=baseconnctor
) )
# Keeps track of when a notification was last sent for streams.
notification_timers_cache = {}
# Keeps track of when streams last went offline.
offline_timer_cache = {}
@classmethod @classmethod
def get_db_upgrade_table(cls) -> UpgradeTable | None: def get_db_upgrade_table(cls) -> UpgradeTable | None:
""" """
@@ -285,15 +302,13 @@ class OwncastSentry(Plugin):
): ):
# Yes! This stream is now live. Send notifications and log it, if allowed. # Yes! This stream is now live. Send notifications and log it, if allowed.
if send_notifications: if send_notifications:
self.log.info( self.log.info(f"[{domain}] Stream is now live!")
f"[{domain}] Stream is now live! Notifying subscribed rooms..."
)
await self.notify_rooms_of_stream_online( await self.notify_rooms_of_stream_online(
domain, new_state["streamTitle"] domain, new_state["streamTitle"], False
) )
else: else:
self.log.info( self.log.info(
f"[{domain}] Stream is live, but performed first update. WIll not notify subscribed rooms." f"[{domain}] Stream is live, but performed first update. Will not notify subscribed rooms."
) )
# Does the latest stream state no longer have a last connect time but the old state does? # Does the latest stream state no longer have a last connect time but the old state does?
@@ -322,14 +337,32 @@ class OwncastSentry(Plugin):
# All done. # All done.
self.log.debug(f"[{domain}] State update completed.") self.log.debug(f"[{domain}] State update completed.")
async def notify_rooms_of_stream_online(self, domain: str, title: str) -> None: async def notify_rooms_of_stream_online(
self, domain: str, title: str, renamed: bool
) -> None:
""" """
Sends notifications to rooms with subscriptions to the provided stream domain. Sends notifications to rooms with subscriptions to the provided stream domain.
:param domain: The domain of the stream to send notifications for. :param domain: The domain of the stream to send notifications for.
:param title: The title of the stream to include in the message. :param title: The title of the stream to include in the message.
:param renamed: Whether or not this is for a stream changing its title rather than going live.
:return: Nothing. :return: Nothing.
""" """
# Has enough time passed since the last notification was sent?
if domain in self.notification_timers_cache:
seconds_since_last_notification = round(
time.time() - self.notification_timers_cache[domain]
)
if seconds_since_last_notification < SECONDS_BETWEEN_NOTIFICATIONS:
self.log.info(
f"[{domain}] Not sending notifications. Only {seconds_since_last_notification} of required {SECONDS_BETWEEN_NOTIFICATIONS} seconds has passed since last notification."
)
return
# Yes. Log the current time and proceed with sending notifications.
self.notification_timers_cache[domain] = time.time()
# Get a list of room IDs with active subscriptions to the stream domain. # Get a list of room IDs with active subscriptions to the stream domain.
query = "SELECT room_id FROM subscriptions WHERE stream_domain=$1" query = "SELECT room_id FROM subscriptions WHERE stream_domain=$1"
async with self.database.acquire() as connection: async with self.database.acquire() as connection: