5 Commits

Author SHA1 Message Date
48b2c30b6d Updated version number in plugin metadata for v1.0.1 release. 2025-03-29 12:45:28 -04:00
05ca37961a Updated version number in user agent for v1.0.1 release. 2025-03-29 12:33:46 -04:00
ad1ae65115 Reorganized stream validation process during subscription changes.
This commit resolves two separate but related issues:

 * Fixes the inability to unsubscribe from domains which are offline or no longer hosting an Owncast stream.

 * Optimizes the subscription process to eliminate redundant HTTP requests. A domain is now validated only once during its first subscription, rather than every subscription attempt.
2025-03-29 12:21:08 -04:00
eeef25c6ed Fixed display of streams with empty titles. 2025-03-29 11:39:20 -04:00
ec13dac64c Corrected license field in plugin metadata to use proper SPDX identifier. 2025-01-28 11:49:35 -05:00
2 changed files with 35 additions and 35 deletions

View File

@@ -1,7 +1,7 @@
maubot: 0.1.0 maubot: 0.1.0
id: dev.logal.owncastsentry id: dev.logal.owncastsentry
version: 1.0.0 version: 1.0.1
license: Apache license: Apache-2.0
modules: modules:
- owncastsentry - owncastsentry
main_class: OwncastSentry main_class: OwncastSentry

View File

@@ -21,7 +21,7 @@ from urllib.parse import urlparse
OWNCAST_STATUS_PATH = "/api/status" OWNCAST_STATUS_PATH = "/api/status"
# User agent to send with all HTTP requests. # User agent to send with all HTTP requests.
USER_AGENT = "OwncastSentry/1.0.0 (bot; +https://git.logal.dev/LogalDeveloper/OwncastSentry)" USER_AGENT = "OwncastSentry/1.0.1 (bot; +https://git.logal.dev/LogalDeveloper/OwncastSentry)"
# ===== DATABASE MIGRATIONS ===== # ===== DATABASE MIGRATIONS =====
@@ -97,16 +97,24 @@ class OwncastSentry(Plugin):
:return: Nothing. :return: Nothing.
""" """
# Convert the semi-unpredictable user input to only a domain and verify it's an Owncast stream. # Convert the user input to only a domain.
stream_domain = await self.validate_url_as_owncast_stream(url) stream_domain = self.domainify(url)
# Did the validation return an empty string? # How many subscriptions already exist for this domain?
if stream_domain == "": query = "SELECT COUNT(*) FROM subscriptions WHERE stream_domain=$1"
# Yes, it's not valid. Tell the user and give up. async with self.database.acquire() as connection:
result = await connection.fetchrow(query, stream_domain)
if result[0] == 0:
# There are 0 subscriptions, we need to validate this domain is an Owncast stream.
# Attempt to fetch the stream state from this domain.
stream_state = await self.get_current_stream_state(stream_domain)
if len(stream_state) == 0:
# The stream state fetch returned nothing. Probably not an Owncast stream.
await evt.reply("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.") await evt.reply("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.")
return return
# Try to add a new subscription for the requested stream domain in the room the command executed in. # Try to add a new subscription for the requested stream domain in the room the command was executed in.
try: try:
query = "INSERT INTO subscriptions (stream_domain, room_id) VALUES ($1, $2)" query = "INSERT INTO subscriptions (stream_domain, room_id) VALUES ($1, $2)"
async with self.database.acquire() as connection: async with self.database.acquire() as connection:
@@ -153,14 +161,8 @@ class OwncastSentry(Plugin):
:param url: A string containing the user supplied URL to a stream to try and unsubscribe from. :param url: A string containing the user supplied URL to a stream to try and unsubscribe from.
:return: Nothing. :return: Nothing.
""" """
# Convert the user input to only a domain and verify it's an Owncast stream. # Convert the user input to only a domain.
stream_domain = await self.validate_url_as_owncast_stream(url) stream_domain = self.domainify(url)
# Did the validation return an empty string?
if stream_domain == "":
# Yes, it's not valid. Tell the user and give up.
await evt.reply("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.")
return
# Attempt to delete the requested subscription from the database. # Attempt to delete the requested subscription from the database.
query = "DELETE FROM subscriptions WHERE stream_domain=$1 AND room_id=$2" query = "DELETE FROM subscriptions WHERE stream_domain=$1 AND room_id=$2"
@@ -279,9 +281,17 @@ class OwncastSentry(Plugin):
successful_notifications = 0 successful_notifications = 0
failed_notifications = 0 failed_notifications = 0
# Build the message body text.
body_text = "🎥 " + domain + " is now live!"
# Streams can have no title. If there is none, don't even mention it.
if title != "":
body_text += "\nStream Title: " + title
body_text += "\n\nTo tune in, visit: https://" + domain + "/"
# Iterate over the subscribed rooms and try to send a message to each. # Iterate over the subscribed rooms and try to send a message to each.
# TODO: This should probably be made async. # TODO: This should probably be made async.
body_text = "🎥 " + domain + " is now live!\nStream Title: " + title +"\n\nTo tune in, visit: https://" + domain + "/"
for rows in results: for rows in results:
room_id = rows["room_id"] room_id = rows["room_id"]
try: try:
@@ -350,15 +360,13 @@ class OwncastSentry(Plugin):
return new_state return new_state
def domainify(self, url) -> str:
async def validate_url_as_owncast_stream(self, url) -> str:
""" """
Take a given URL and validate its domain is a valid Owncast stream. Take a given URL and convert it to just the domain.
:param url: A URL with the domain to check for an Owncast stream. :param url:
:return: A string with just the domain if it contains an Owncast stream, or an empty string if an error occurred or is otherwise not valid. :return:
""" """
# Take whatever input the user provided and try to turn it into just the domain. # Take whatever input the user provided and try to turn it into just the domain.
# Examples: # Examples:
# "stream.logal.dev" -> "stream.logal.dev" # "stream.logal.dev" -> "stream.logal.dev"
@@ -367,12 +375,4 @@ class OwncastSentry(Plugin):
# "https://stream.logal.dev/abcdefghijklmno/123456789" -> "stream.logal.dev # "https://stream.logal.dev/abcdefghijklmno/123456789" -> "stream.logal.dev
parsed_url = urlparse(url) parsed_url = urlparse(url)
domain = (parsed_url.netloc or parsed_url.path).lower() domain = (parsed_url.netloc or parsed_url.path).lower()
# Try to fetch the current status of the stream at the given domain.
stream_state = await self.get_current_stream_state(domain)
# The above method does all the checking. If the length of the output dictionary is more than 0, it should be valid. Otherwise, pass an empty string to say it's invalid.
if len(stream_state) > 0:
return domain return domain
else:
return ""