diff --git a/config.dist.ini b/config.dist.ini index 6617fa5..e6b4ea4 100644 --- a/config.dist.ini +++ b/config.dist.ini @@ -69,4 +69,10 @@ AccessToken = syt_yoursynapsetoken # # UserID = @rssbot:matrix.local +# The event type to use for sending messages +# Can be either "text" or "notice" +# Defaults to "notice", can be overridden using the `eventtype` command +# +# EventType = notice + ############################################################################### diff --git a/pantalaimon.example.conf b/contrib/pantalaimon.example.conf similarity index 62% rename from pantalaimon.example.conf rename to contrib/pantalaimon.example.conf index 5cc48fd..e43a0a1 100644 --- a/pantalaimon.example.conf +++ b/contrib/pantalaimon.example.conf @@ -1,5 +1,5 @@ [Homeserver] Homeserver = https://example.com ListenAddress = localhost -ListenPort = 8010 +ListenPort = 8009 IgnoreVerification = True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4201adf..2498d27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,20 +7,16 @@ allow-direct-references = true [project] name = "matrix-rssbot" -version = "0.1.1" +version = "0.1.2" -authors = [ - { name="Private.coffee Team", email="support@private.coffee" }, -] +authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }] description = "Simple RSS feed bridge for Matrix" readme = "README.md" -license = { file="LICENSE" } +license = { file = "LICENSE" } requires-python = ">=3.10" -packages = [ - "src/matrix_rssbot" -] +packages = ["src/matrix_rssbot"] classifiers = [ "Programming Language :: Python :: 3", @@ -29,12 +25,12 @@ classifiers = [ ] dependencies = [ - "matrix-nio", - "pantalaimon", + "matrix-nio>=0.24.0", "setuptools", "markdown2", "feedparser", - ] + "future>=1.0.0", +] [project.urls] "Homepage" = "https://git.private.coffee/PrivateCoffee/matrix-rssbot" @@ -44,4 +40,4 @@ dependencies = [ rssbot = "matrix_rssbot.__main__:main" [tool.hatch.build.targets.wheel] -packages = ["src/matrix_rssbot"] \ No newline at end of file +packages = ["src/matrix_rssbot"] diff --git a/rssbot-pantalaimon.service b/rssbot-pantalaimon.service deleted file mode 100644 index 1110216..0000000 --- a/rssbot-pantalaimon.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=Pantalaimon for RSSbot -Requires=network.target - -[Service] -Type=simple -User=rssbot -Group=rssbot -WorkingDirectory=/opt/rssbot -ExecStart=/opt/rssbot/venv/bin/python3 -um pantalaimon.main -c pantalaimon.conf -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/src/matrix_rssbot/classes/bot.py b/src/matrix_rssbot/classes/bot.py index 6a7c962..7544a12 100644 --- a/src/matrix_rssbot/classes/bot.py +++ b/src/matrix_rssbot/classes/bot.py @@ -1,5 +1,4 @@ import asyncio -import functools from nio import ( AsyncClient, @@ -10,27 +9,12 @@ from nio import ( Response, MatrixRoom, Api, - RoomMessagesError, - GroupEncryptionError, - EncryptionError, RoomMessageText, RoomSendResponse, SyncResponse, - RoomMessageNotice, JoinError, RoomLeaveError, RoomSendError, - RoomVisibility, - RoomCreateError, - RoomMessageMedia, - RoomMessageImage, - RoomMessageFile, - RoomMessageAudio, - DownloadError, - DownloadResponse, - ToDeviceEvent, - ToDeviceError, - RoomPutStateError, RoomGetStateError, ) @@ -38,16 +22,10 @@ from typing import Optional, List from configparser import ConfigParser from datetime import datetime from io import BytesIO -from pathlib import Path -from contextlib import closing -import base64 import uuid import traceback import json -import importlib.util -import sys -import traceback import markdown2 import feedparser @@ -86,9 +64,18 @@ class RSSBot: """ try: return json.loads(self.config["RSSBot"]["AllowedUsers"]) - except: + except Exception: return [] + @property + def event_type(self) -> str: + """Event type of outgoing messages. + + Returns: + str: The event type of outgoing messages. Either "text" or "notice". + """ + return self.config["Matrix"].get("EventType", "notice") + @property def display_name(self) -> str: """Display name of the bot user. @@ -280,7 +267,7 @@ class RSSBot: ) return - task = asyncio.create_task(self._event_callback(room, event)) + asyncio.create_task(self._event_callback(room, event)) async def _response_callback(self, response: Response): for response_type, callback in RESPONSE_CALLBACKS.items(): @@ -288,7 +275,7 @@ class RSSBot: await callback(response, self) async def response_callback(self, response: Response): - task = asyncio.create_task(self._response_callback(response)) + asyncio.create_task(self._response_callback(response)) async def accept_pending_invites(self): """Accept all pending invites.""" @@ -355,89 +342,6 @@ class RSSBot: return response.content_uri - async def send_image( - self, room: MatrixRoom, image: bytes, message: Optional[str] = None - ): - """Send an image to a room. - - Args: - room (MatrixRoom|str): The room to send the image to. - image (bytes): The image to send. - message (str, optional): The message to send with the image. Defaults to None. - """ - - if isinstance(room, MatrixRoom): - room = room.room_id - - self.logger.log( - f"Sending image of size {len(image)} bytes to room {room}", "debug" - ) - - bio = BytesIO(image) - img = Image.open(bio) - mime = Image.MIME[img.format] - - (width, height) = img.size - - self.logger.log( - f"Uploading - Image size: {width}x{height} pixels, MIME type: {mime}", - "debug", - ) - - content_uri = await self.upload_file(image, "image", mime) - - self.logger.log("Uploaded image - sending message...", "debug") - - content = { - "body": message or "", - "info": { - "mimetype": mime, - "size": len(image), - "w": width, - "h": height, - }, - "msgtype": "m.image", - "url": content_uri, - } - - status = await self.matrix_client.room_send(room, "m.room.message", content) - - self.logger.log("Sent image", "debug") - - async def send_file( - self, room: MatrixRoom, file: bytes, filename: str, mime: str, msgtype: str - ): - """Send a file to a room. - - Args: - room (MatrixRoom|str): The room to send the file to. - file (bytes): The file to send. - filename (str): The name of the file. - mime (str): The MIME type of the file. - """ - - if isinstance(room, MatrixRoom): - room = room.room_id - - self.logger.log( - f"Sending file of size {len(file)} bytes to room {room}", "debug" - ) - - content_uri = await self.upload_file(file, filename, mime) - - self.logger.log("Uploaded file - sending message...", "debug") - - content = { - "body": filename, - "info": {"mimetype": mime, "size": len(file)}, - "msgtype": msgtype, - "url": content_uri, - } - - status = await self.matrix_client.room_send(room, "m.room.message", content) - - self.logger.log("Sent file", "debug") - async def send_message( self, room: MatrixRoom | str, @@ -475,11 +379,8 @@ class RSSBot: "content": message, } - content = None - - if not content: - msgtype = "m.room.message" - content = msgcontent + msgtype = "m.room.message" + content = msgcontent method, path, data = Api.room_send( self.matrix_client.access_token, @@ -528,6 +429,22 @@ class RSSBot: if state_key is None or event["state_key"] == state_key: return event + async def get_event_type_for_room(self, room: MatrixRoom) -> str: + """Returns the event type to use for a room + + Either the default event type or the event type set in the room's state + + Args: + room (MatrixRoom): The room to get the event type for + + Returns: + str: The event type to use + """ + state = await self.get_state_event(room, "rssbot.event_type") + if state: + return state["content"]["event_type"] + return self.event_type + async def process_room(self, room): self.logger.log(f"Processing room {room}", "debug") @@ -558,7 +475,7 @@ class RSSBot: for entry in feed_content.entries: try: entry_time_info = entry.published_parsed - except: + except Exception: entry_time_info = entry.updated_parsed entry_timestamp = int(datetime(*entry_time_info[:6]).timestamp()) @@ -567,7 +484,11 @@ class RSSBot: if entry_timestamp > timestamp: entry_message = f"__{feed_content.feed.title}: {entry.title}__\n\n{entry.description}\n\n{entry.link}" - await self.send_message(room, entry_message) + await self.send_message( + room, + entry_message, + (await self.get_event_type_for_room(room)) == "notice", + ) new_timestamp = max(entry_timestamp, new_timestamp) await self.send_state_event( diff --git a/src/matrix_rssbot/classes/callbacks/__init__.py b/src/matrix_rssbot/classes/callbacks/__init__.py index 3d1f602..3cbd544 100644 --- a/src/matrix_rssbot/classes/callbacks/__init__.py +++ b/src/matrix_rssbot/classes/callbacks/__init__.py @@ -1,13 +1,8 @@ from nio import ( RoomMessageText, InviteEvent, - Event, SyncResponse, - JoinResponse, RoomMemberEvent, - Response, - MegolmEvent, - KeysQueryResponse ) from .sync import sync_callback diff --git a/src/matrix_rssbot/classes/callbacks/invite.py b/src/matrix_rssbot/classes/callbacks/invite.py index 67995bc..4e4b284 100644 --- a/src/matrix_rssbot/classes/callbacks/invite.py +++ b/src/matrix_rssbot/classes/callbacks/invite.py @@ -1,10 +1,11 @@ from nio import InviteEvent, MatrixRoom + async def room_invite_callback(room: MatrixRoom, event: InviteEvent, bot): if room.room_id in bot.matrix_client.rooms: - logging(f"Already in room {room.room_id} - ignoring invite") + bot.logger.log(f"Already in room {room.room_id} - ignoring invite", "debug") return bot.logger.log(f"Received invite to room {room.room_id} - joining...") - response = await bot.matrix_client.join(room.room_id) \ No newline at end of file + await bot.matrix_client.join(room.room_id) diff --git a/src/matrix_rssbot/classes/callbacks/message.py b/src/matrix_rssbot/classes/callbacks/message.py index 256a587..8b69ead 100644 --- a/src/matrix_rssbot/classes/callbacks/message.py +++ b/src/matrix_rssbot/classes/callbacks/message.py @@ -2,6 +2,7 @@ from nio import MatrixRoom, RoomMessageText from datetime import datetime + async def message_callback(room: MatrixRoom | str, event: RoomMessageText, bot): bot.logger.log(f"Received message from {event.sender} in room {room.room_id}") @@ -16,12 +17,18 @@ async def message_callback(room: MatrixRoom | str, event: RoomMessageText, bot): await bot.process_command(room, event) elif event.body.startswith("!"): - bot.logger.log(f"Received {event.body} - might be a command, but not for this bot - ignoring", "debug") + bot.logger.log( + f"Received {event.body} - might be a command, but not for this bot - ignoring", + "debug", + ) else: - bot.logger.log(f"Received regular message - ignoring", "debug") + bot.logger.log("Received regular message - ignoring", "debug") processed = datetime.now() processing_time = processed - received - bot.logger.log(f"Message processing took {processing_time.total_seconds()} seconds (latency: {latency.total_seconds()} seconds)", "debug") + bot.logger.log( + f"Message processing took {processing_time.total_seconds()} seconds (latency: {latency.total_seconds()} seconds)", + "debug", + ) diff --git a/src/matrix_rssbot/classes/callbacks/roommember.py b/src/matrix_rssbot/classes/callbacks/roommember.py index 3abf7de..d691adf 100644 --- a/src/matrix_rssbot/classes/callbacks/roommember.py +++ b/src/matrix_rssbot/classes/callbacks/roommember.py @@ -1,8 +1,12 @@ -from nio import RoomMemberEvent, MatrixRoom, KeysUploadError +from nio import RoomMemberEvent, MatrixRoom + async def roommember_callback(room: MatrixRoom, event: RoomMemberEvent, bot): if event.membership == "leave": - bot.logger.log(f"User {event.state_key} left room {room.room_id} - am I alone now?", "debug") + bot.logger.log( + f"User {event.state_key} left room {room.room_id} - am I alone now?", + "debug", + ) if len(room.users) == 1: bot.logger.log("Yes, I was abandoned - leaving...", "debug") diff --git a/src/matrix_rssbot/classes/callbacks/sync.py b/src/matrix_rssbot/classes/callbacks/sync.py index 61df74d..046ffdb 100644 --- a/src/matrix_rssbot/classes/callbacks/sync.py +++ b/src/matrix_rssbot/classes/callbacks/sync.py @@ -1,7 +1,8 @@ async def sync_callback(response, bot): bot.logger.log( - f"Sync response received (next batch: {response.next_batch})", "debug") + f"Sync response received (next batch: {response.next_batch})", "debug" + ) bot.sync_response = response - await bot.accept_pending_invites() \ No newline at end of file + await bot.accept_pending_invites() diff --git a/src/matrix_rssbot/classes/commands/__init__.py b/src/matrix_rssbot/classes/commands/__init__.py index 3298263..8217661 100644 --- a/src/matrix_rssbot/classes/commands/__init__.py +++ b/src/matrix_rssbot/classes/commands/__init__.py @@ -6,7 +6,6 @@ COMMANDS = {} for command in [ "help", - "newroom", "botinfo", "privacy", "addfeed", @@ -14,8 +13,10 @@ for command in [ "processfeeds", "removefeed", ]: - function = getattr(import_module( - "." + command, "matrix_rssbot.classes.commands"), "command_" + command) + function = getattr( + import_module("." + command, "matrix_rssbot.classes.commands"), + "command_" + command, + ) COMMANDS[command] = function COMMANDS[None] = command_unknown diff --git a/src/matrix_rssbot/classes/commands/addfeed.py b/src/matrix_rssbot/classes/commands/addfeed.py index 16ab691..d4732f0 100644 --- a/src/matrix_rssbot/classes/commands/addfeed.py +++ b/src/matrix_rssbot/classes/commands/addfeed.py @@ -3,7 +3,6 @@ from nio import RoomPutStateError from nio.rooms import MatrixRoom from datetime import datetime -from urllib.request import urlopen import feedparser @@ -26,11 +25,12 @@ async def command_addfeed(room: MatrixRoom, event: RoomMessageText, bot): try: feedparser.parse(url) - except: + except Exception as e: await bot.send_state_event( f"Could not access or parse feed at {url}. Please ensure that you got the URL right, and that it is actually an RSS/Atom feed.", True, ) + bot.logger.log(f"Could not access or parse feed at {url}: {e}", "error") try: response1 = await bot.send_state_event( @@ -50,6 +50,9 @@ async def command_addfeed(room: MatrixRoom, event: RoomMessageText, bot): await bot.send_message( room, "Unable to write feed state to the room. Please try again.", True ) + bot.logger.log( + f"Error adding feed to room {room.room_id}: {response1.error}", "error" + ) return response2 = await bot.send_state_event(room, "rssbot.feeds", {"feeds": feeds}) @@ -58,10 +61,14 @@ async def command_addfeed(room: MatrixRoom, event: RoomMessageText, bot): await bot.send_message( room, "Unable to write feed list to the room. Please try again.", True ) + bot.logger.log( + f"Error adding feed to room {room.room_id}: {response2.error}", "error" + ) return await bot.send_message(room, f"Added {url} to this room's feeds.", True) - except: + except Exception as e: await bot.send_message( - room, "Sorry, something went wrong. Please try again.", true + room, "Sorry, something went wrong. Please try again.", True ) + bot.logger.log(f"Error adding feed to room {room.room_id}: {e}", "error") \ No newline at end of file diff --git a/src/matrix_rssbot/classes/commands/botinfo.py b/src/matrix_rssbot/classes/commands/botinfo.py index 5e725db..e6167c6 100644 --- a/src/matrix_rssbot/classes/commands/botinfo.py +++ b/src/matrix_rssbot/classes/commands/botinfo.py @@ -3,7 +3,7 @@ from nio.rooms import MatrixRoom async def command_botinfo(room: MatrixRoom, event: RoomMessageText, bot): - logging("Showing bot info...") + bot.logger.log("Showing bot info...", "debug") body = f""" Room info: diff --git a/src/matrix_rssbot/classes/commands/help.py b/src/matrix_rssbot/classes/commands/help.py index 5477d69..2c9295a 100644 --- a/src/matrix_rssbot/classes/commands/help.py +++ b/src/matrix_rssbot/classes/commands/help.py @@ -11,7 +11,6 @@ async def command_help(room: MatrixRoom, event: RoomMessageText, bot): - !rssbot addfeed \ - Bridges a new feed to the current room - !rssbot listfeeds - Lists all bridged feeds - !rssbot removefeed \ - Removes a bridged feed given the numeric index from the listfeeds command or the URL of the feed -- !rssbot newroom \ - Create a new room and invite yourself to it """ await bot.send_message(room, body, True) diff --git a/src/matrix_rssbot/classes/commands/listfeeds.py b/src/matrix_rssbot/classes/commands/listfeeds.py index 4c6cb2b..3494518 100644 --- a/src/matrix_rssbot/classes/commands/listfeeds.py +++ b/src/matrix_rssbot/classes/commands/listfeeds.py @@ -1,12 +1,11 @@ from nio.events.room_events import RoomMessageText -from nio import RoomPutStateError from nio.rooms import MatrixRoom async def command_listfeeds(room: MatrixRoom, event: RoomMessageText, bot): state = await bot.get_state_event(room, "rssbot.feeds") if (not state) or (not state["content"]["feeds"]): - message = f"There are currently no feeds associated with this room." + message = "There are currently no feeds associated with this room." else: message = "This room is currently bridged to the following feeds:\n\n" diff --git a/src/matrix_rssbot/classes/commands/newroom.py b/src/matrix_rssbot/classes/commands/newroom.py deleted file mode 100644 index 170b3a2..0000000 --- a/src/matrix_rssbot/classes/commands/newroom.py +++ /dev/null @@ -1,31 +0,0 @@ -from nio.events.room_events import RoomMessageText -from nio import RoomCreateError, RoomInviteError -from nio.rooms import MatrixRoom - -from contextlib import closing - -async def command_newroom(room: MatrixRoom, event: RoomMessageText, bot): - room_name = " ".join(event.body.split()[ - 2:]) or bot.default_room_name - - bot.logger.log("Creating new room...") - new_room = await bot.matrix_client.room_create(name=room_name) - - if isinstance(new_room, RoomCreateError): - bot.logger.log(f"Failed to create room: {new_room.message}") - await bot.send_message(room, f"Sorry, I was unable to create a new room. Please try again later, or create a room manually.", True) - return - - bot.logger.log(f"Inviting {event.sender} to new room...") - invite = await bot.matrix_client.room_invite(new_room.room_id, event.sender) - - if isinstance(invite, RoomInviteError): - bot.logger.log(f"Failed to invite user: {invite.message}") - await bot.send_message(room, f"Sorry, I was unable to invite you to the new room. Please try again later, or create a room manually.", True) - return - - await bot.matrix_client.room_put_state( - new_room.room_id, "m.room.power_levels", {"users": {event.sender: 100, bot.matrix_client.user_id: 100}}) - - await bot.matrix_client.joined_rooms() - await bot.send_message(room, f"Alright, I've created a new room called '{room_name}' and invited you to it. You can find it at {new_room.room_id}", True) diff --git a/src/matrix_rssbot/classes/commands/privacy.py b/src/matrix_rssbot/classes/commands/privacy.py index 3b63c8e..fa7ad1a 100644 --- a/src/matrix_rssbot/classes/commands/privacy.py +++ b/src/matrix_rssbot/classes/commands/privacy.py @@ -5,7 +5,9 @@ from nio.rooms import MatrixRoom async def command_privacy(room: MatrixRoom, event: RoomMessageText, bot): body = "**Privacy**\n\nIf you use this bot, note that your messages will be sent to the following recipients:\n\n" - body += "- The bot's operator" + (f"({bot.operator})" if bot.operator else "") + "\n" + body += ( + "- The bot's operator" + (f"({bot.operator})" if bot.operator else "") + "\n" + ) body += "- The operator(s) of the involved Matrix homeservers\n" - await bot.send_message(room, body, True) \ No newline at end of file + await bot.send_message(room, body, True) diff --git a/src/matrix_rssbot/classes/commands/processfeeds.py b/src/matrix_rssbot/classes/commands/processfeeds.py index e475f07..7e89ac0 100644 --- a/src/matrix_rssbot/classes/commands/processfeeds.py +++ b/src/matrix_rssbot/classes/commands/processfeeds.py @@ -1,5 +1,4 @@ from nio.events.room_events import RoomMessageText -from nio import RoomPutStateError from nio.rooms import MatrixRoom async def command_processfeeds(room: MatrixRoom, event: RoomMessageText, bot): diff --git a/src/matrix_rssbot/classes/commands/removefeed.py b/src/matrix_rssbot/classes/commands/removefeed.py index 006ec79..14adabf 100644 --- a/src/matrix_rssbot/classes/commands/removefeed.py +++ b/src/matrix_rssbot/classes/commands/removefeed.py @@ -1,5 +1,4 @@ from nio.events.room_events import RoomMessageText -from nio import RoomPutStateError from nio.rooms import MatrixRoom async def command_removefeed(room: MatrixRoom, event: RoomMessageText, bot): @@ -14,7 +13,7 @@ async def command_removefeed(room: MatrixRoom, event: RoomMessageText, bot): if identifier.isnumeric(): try: - feed = feeds.pop(int(identifier)) + feeds.pop(int(identifier)) except IndexError: await bot.send_message(room, f"There is no feed with index {identifier}.") return @@ -22,7 +21,7 @@ async def command_removefeed(room: MatrixRoom, event: RoomMessageText, bot): try: feeds.remove(identifier) except ValueError: - await bot.send_message(room, f"There is no bridged feed with the provided URL.") + await bot.send_message(room, "There is no bridged feed with the provided URL.") return await bot.send_state_event(room, "rssbot.feeds", {"feeds": feeds}) \ No newline at end of file diff --git a/src/matrix_rssbot/classes/commands/unknown.py b/src/matrix_rssbot/classes/commands/unknown.py index fd9e5f9..9bc3af5 100644 --- a/src/matrix_rssbot/classes/commands/unknown.py +++ b/src/matrix_rssbot/classes/commands/unknown.py @@ -5,4 +5,4 @@ from nio.rooms import MatrixRoom async def command_unknown(room: MatrixRoom, event: RoomMessageText, bot): bot.logger.log("Unknown command") - await bot.send_message(room, "Unknown command - try !rssbot help", True) \ No newline at end of file + await bot.send_message(room, "Unknown command - try !rssbot help", True) diff --git a/src/matrix_rssbot/classes/logging.py b/src/matrix_rssbot/classes/logging.py index 5978fa1..ca4e092 100644 --- a/src/matrix_rssbot/classes/logging.py +++ b/src/matrix_rssbot/classes/logging.py @@ -9,14 +9,16 @@ class Logger: def __init__(self, log_level: str = "warning"): if log_level not in self.LOG_LEVELS: raise ValueError( - f"Invalid log level {log_level}. Valid levels are {', '.join(self.LOG_LEVELS)}") + f"Invalid log level {log_level}. Valid levels are {', '.join(self.LOG_LEVELS)}" + ) self.log_level = log_level def log(self, message: str, log_level: str = "info"): if log_level not in self.LOG_LEVELS: raise ValueError( - f"Invalid log level {log_level}. Valid levels are {', '.join(self.LOG_LEVELS)}") + f"Invalid log level {log_level}. Valid levels are {', '.join(self.LOG_LEVELS)}" + ) if self.LOG_LEVELS.index(log_level) < self.LOG_LEVELS.index(self.log_level): return