feat: add message event type configuration

Added support for configurable event types for sending messages, enabling use of either "text" or "notice" message types as per user preference. Additionally, this update brings along improvements including code cleanup, updated dependencies to improve functionality and fix potential security issues, and reorganization of example configuration files into a more structured directory. Significant refactorings standardize async task creations and remove unused imports, streamlining the codebase for better maintainability and performance.

- Introduced `EventType` in `config.dist.ini` to allow users to specify the desired event type for outbound messages.
- Upgraded dependency versions in `pyproject.toml`, adding `future` for forward compatibility.
- Removed the deprecated `rssbot-pantalaimon.service` and unused image/file send functions in `bot.py`, focusing on core functionalities.
- Reorganized `pantalaimon.example.conf` into `contrib` directory for better project structure.
- Simplified logging and callback handling, removing redundant code paths and improving code readability.
- Bumped project version to 0.1.2, paving the way for new features and fixes.

This enhancement is aimed at providing users with greater flexibility in how messages are sent, accommodating different use cases and preferences.
This commit is contained in:
Kumi 2024-05-17 16:49:27 +02:00
parent 127cfc55e7
commit 7c8d957548
Signed by: kumi
GPG key ID: ECBCC9082395383F
21 changed files with 101 additions and 208 deletions

View file

@ -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
###############################################################################

View file

@ -1,5 +1,5 @@
[Homeserver]
Homeserver = https://example.com
ListenAddress = localhost
ListenPort = 8010
ListenPort = 8009
IgnoreVerification = True

View file

@ -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"]
packages = ["src/matrix_rssbot"]

View file

@ -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

View file

@ -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(

View file

@ -1,13 +1,8 @@
from nio import (
RoomMessageText,
InviteEvent,
Event,
SyncResponse,
JoinResponse,
RoomMemberEvent,
Response,
MegolmEvent,
KeysQueryResponse
)
from .sync import sync_callback

View file

@ -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)
await bot.matrix_client.join(room.room_id)

View file

@ -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",
)

View file

@ -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")

View file

@ -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()
await bot.accept_pending_invites()

View file

@ -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

View file

@ -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")

View file

@ -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:

View file

@ -11,7 +11,6 @@ async def command_help(room: MatrixRoom, event: RoomMessageText, bot):
- !rssbot addfeed \<url\> - Bridges a new feed to the current room
- !rssbot listfeeds - Lists all bridged feeds
- !rssbot removefeed \<index|url\> - Removes a bridged feed given the numeric index from the listfeeds command or the URL of the feed
- !rssbot newroom \<room name\> - Create a new room and invite yourself to it
"""
await bot.send_message(room, body, True)

View file

@ -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"

View file

@ -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)

View file

@ -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)
await bot.send_message(room, body, True)

View file

@ -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):

View file

@ -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})

View file

@ -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)
await bot.send_message(room, "Unknown command - try !rssbot help", True)

View file

@ -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