feat: transform Matrix-RSSBot into Matrix-ReportBot

This major overhaul shifts the project's focus from serving RSS feeds to reporting content within the Matrix ecosystem. Key changes include:
- Renaming from Matrix-RSSBot to Matrix-ReportBot, aligning the project with its new purpose.
- Removal of RSS related functionalities and incorporation of report handling capabilities, facilitating automated content reporting and monitoring in Matrix rooms.
- Introduction of new dependencies and configurations specific to the report handling features.
- Updating service configurations to ensure compatibility with the bot's new functionality.

This transformation addresses the need for a specialized tool in the Matrix community for managing and automating content reports, enhancing moderation efficiency and response time.
This commit is contained in:
Kumi 2024-05-05 19:21:18 +02:00
parent 02d7441f38
commit 8a31063bb2
Signed by: kumi
GPG key ID: ECBCC9082395383F
28 changed files with 205 additions and 628 deletions

3
.gitignore vendored
View file

@ -4,4 +4,5 @@ config.ini
__pycache__/ __pycache__/
dist/ dist/
pantalaimon.conf pantalaimon.conf
build/ build/
venv/

View file

@ -1,17 +1,17 @@
# Matrix-RSSBot # Matrix-ReportBot
[![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-support%20us!-pink?logo=coffeescript)](https://private.coffee) [![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-support%20us!-pink?logo=coffeescript)](https://private.coffee)
[![PyPI](https://shields.private.coffee/pypi/v/matrix-rssbot)](https://pypi.org/project/matrix-rssbot/) [![PyPI](https://shields.private.coffee/pypi/v/matrix-reportbot)](https://pypi.org/project/matrix-reportbot/)
[![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/matrix-rssbot)](https://pypi.org/project/matrix-rssbot/) [![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/matrix-reportbot)](https://pypi.org/project/matrix-reportbot/)
[![PyPI - License](https://shields.private.coffee/pypi/l/matrix-rssbot)](https://pypi.org/project/matrix-rssbot/) [![PyPI - License](https://shields.private.coffee/pypi/l/matrix-reportbot)](https://pypi.org/project/matrix-reportbot/)
[![Latest Git Commit](https://shields.private.coffee/gitea/last-commit/privatecoffee/matrix-rssbot?gitea_url=https://git.private.coffee)](https://git.private.coffee/privatecoffee/matrix-rssbot) [![Latest Git Commit](https://shields.private.coffee/gitea/last-commit/privatecoffee/matrix-reportbot?gitea_url=https://git.private.coffee)](https://git.private.coffee/privatecoffee/matrix-reportbot)
This is a simple, no-database RSS/Atom feed bot for Matrix. It is designed to be easy to use and lightweight. This is a simple bot that can be used to display incoming moderation reports in a Matrix room.
## Installation ## Installation
```bash ```bash
pip install matrix-rssbot pip install matrix-reportbot
``` ```
Create a configuration file in `config.ini` based on the [config.dist.ini](config.dist.ini) provided in the repository. Create a configuration file in `config.ini` based on the [config.dist.ini](config.dist.ini) provided in the repository.
@ -22,42 +22,19 @@ At the very least, you need to provide the following configuration:
[Matrix] [Matrix]
Homeserver = http://your-homeserver.example.com Homeserver = http://your-homeserver.example.com
AccessToken = syt_YourAccessTokenHere AccessToken = syt_YourAccessTokenHere
RoomId = !yourRoomId:your-homeserver.example.com
``` ```
Note: The AccessToken must be for a admin user, because the bot needs to be able to read the moderation events.
We recommend using pantalaimon as a proxy, because the bot itself does not support end-to-end encryption. We recommend using pantalaimon as a proxy, because the bot itself does not support end-to-end encryption.
You can start the bot by running: You can start the bot by running:
```bash ```bash
rssbot reportbot
``` ```
## Usage
The bot will automatically join all rooms it is invited to.
You have to ensure that the bot has the necessary permissions to send state events and messages in the room. Regular users cannot send state events, so you have to either raise the bot user's power level (`Moderator` level should do) or lower the power level required to send state events.
You can now add a feed to the bot by sending a message to the bot in the room you want the feed to be posted in. The message should be in the following format:
```
!rssbot addfeed https://example.com/feed.xml
```
To list all feeds in a room, you can use the following command:
```
!rssbot listfeeds
```
Finally, to remove a feed, you can use the following command:
```
!rssbot removefeed https://example.com/feed.xml
```
Alternatively, you can use the number of the feed in the list, which you can get by using the `listfeeds` command instead of the URL.
## License ## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

View file

@ -5,7 +5,7 @@
############################################################################### ###############################################################################
[RSSBot] [ReportBot]
# Some way for the user to contact you. # Some way for the user to contact you.
# Ideally, either your personal user ID or a support room # Ideally, either your personal user ID or a support room
@ -24,20 +24,9 @@ Operator = Contact details not set
# #
# Debug = 1 # Debug = 1
# The default room name used by the !newroom command
# Defaults to RSSBot if not set
#
# DefaultRoomName = RSSBot
# Display name for the bot # Display name for the bot
# #
# DisplayName = RSSBot # DisplayName = ReportBot
# A list of allowed users
# If not defined, everyone is allowed to use the bot
# Use the "*:homeserver.matrix" syntax to allow everyone on a given homeserver
#
# AllowedUsers = ["*:matrix.local"]
# Minimum level of log messages that should be printed # Minimum level of log messages that should be printed
# Available log levels in ascending order: trace, debug, info, warning, error, critical # Available log levels in ascending order: trace, debug, info, warning, error, critical
@ -64,6 +53,10 @@ Homeserver = https://matrix.local
# #
AccessToken = syt_yoursynapsetoken AccessToken = syt_yoursynapsetoken
# The room ID in which reports should be posted
#
RoomID = !yourroomid:matrix.local
# The Matrix user ID of the bot (@local:domain.tld) # The Matrix user ID of the bot (@local:domain.tld)
# Only specify this if the bot fails to figure it out by itself # Only specify this if the bot fails to figure it out by itself
# #

View file

@ -6,20 +6,20 @@ build-backend = "hatchling.build"
allow-direct-references = true allow-direct-references = true
[project] [project]
name = "matrix-rssbot" name = "matrix-reportbot"
version = "0.1.1" version = "0.0.1"
authors = [ authors = [
{ name="Private.coffee Team", email="support@private.coffee" }, { name="Private.coffee Team", email="support@private.coffee" },
] ]
description = "Simple RSS feed bridge for Matrix" description = "Simple content report subscription bot for Matrix."
readme = "README.md" readme = "README.md"
license = { file="LICENSE" } license = { file="LICENSE" }
requires-python = ">=3.10" requires-python = ">=3.10"
packages = [ packages = [
"src/matrix_rssbot" "src/matrix_reportbot"
] ]
classifiers = [ classifiers = [
@ -33,15 +33,14 @@ dependencies = [
"pantalaimon", "pantalaimon",
"setuptools", "setuptools",
"markdown2", "markdown2",
"feedparser",
] ]
[project.urls] [project.urls]
"Homepage" = "https://git.private.coffee/PrivateCoffee/matrix-rssbot" "Homepage" = "https://git.private.coffee/PrivateCoffee/matrix-reportbot"
"Bug Tracker" = "https://git.private.coffee/PrivateCoffee/matrix-rssbot/issues" "Bug Tracker" = "https://git.private.coffee/PrivateCoffee/matrix-reportbot/issues"
[project.scripts] [project.scripts]
rssbot = "matrix_rssbot.__main__:main" reportbot = "matrix_reportbot.__main__:main"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/matrix_rssbot"] packages = ["src/matrix_reportbot"]

View file

@ -0,0 +1,15 @@
[Unit]
Description=Pantalaimon for ReportBot
Requires=network.target
[Service]
Type=simple
User=reportbot
Group=reportbot
WorkingDirectory=/opt/reportbot
ExecStart=/opt/reportbot/venv/bin/python3 -um pantalaimon.main -c pantalaimon.conf
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

15
reportbot.service Normal file
View file

@ -0,0 +1,15 @@
[Unit]
Description=ReportBot
Requires=network.target
[Service]
Type=simple
User=reportbot
Group=reportbot
WorkingDirectory=/opt/reportbot
ExecStart=/opt/reportbot/.venv/bin/python3 -um reportbot
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

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,15 +0,0 @@
[Unit]
Description=RSSbot
Requires=network.target
[Service]
Type=simple
User=rssbot
Group=rssbot
WorkingDirectory=/opt/rssbot
ExecStart=/opt/rssbot/.venv/bin/python3 -um rssbot
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View file

@ -1,4 +1,4 @@
from .classes.bot import RSSBot from .classes.bot import ReportBot
from argparse import ArgumentParser from argparse import ArgumentParser
from configparser import ConfigParser from configparser import ConfigParser
@ -12,7 +12,7 @@ def sigterm_handler(_signo, _stack_frame):
def get_version(): def get_version():
try: try:
package_version = pkg_resources.get_distribution("matrix_rssbot").version package_version = pkg_resources.get_distribution("matrix_reportbot").version
except pkg_resources.DistributionNotFound: except pkg_resources.DistributionNotFound:
return None return None
return package_version return package_version
@ -40,7 +40,7 @@ def main():
config.read(args.config) config.read(args.config)
# Create bot # Create bot
bot = RSSBot.from_config(config) bot = ReportBot.from_config(config)
# Listen for SIGTERM # Listen for SIGTERM
signal.signal(signal.SIGTERM, sigterm_handler) signal.signal(signal.SIGTERM, sigterm_handler)

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
import functools import functools
import aiohttp
from nio import ( from nio import (
AsyncClient, AsyncClient,
@ -50,14 +51,11 @@ import sys
import traceback import traceback
import markdown2 import markdown2
import feedparser
from .logging import Logger from .logging import Logger
from .callbacks import RESPONSE_CALLBACKS, EVENT_CALLBACKS
from .commands import COMMANDS
class RSSBot: class ReportBot:
# Default values # Default values
matrix_client: Optional[AsyncClient] = None matrix_client: Optional[AsyncClient] = None
sync_token: Optional[str] = None sync_token: Optional[str] = None
@ -75,7 +73,7 @@ class RSSBot:
@property @property
def loop_duration(self) -> int: def loop_duration(self) -> int:
return self.config["RSSBot"].getint("LoopDuration", 300) return self.config["ReportBot"].getint("LoopDuration", 300)
@property @property
def allowed_users(self) -> List[str]: def allowed_users(self) -> List[str]:
@ -85,18 +83,27 @@ class RSSBot:
List[str]: List of user IDs. Defaults to [], which means all users are allowed. List[str]: List of user IDs. Defaults to [], which means all users are allowed.
""" """
try: try:
return json.loads(self.config["RSSBot"]["AllowedUsers"]) return json.loads(self.config["ReportBot"]["AllowedUsers"])
except: except:
return [] return []
@property
def room_id(self) -> str:
"""Room ID to send reports to.
Returns:
str: The room ID to send reports to.
"""
return self.config["Matrix"]["RoomID"]
@property @property
def display_name(self) -> str: def display_name(self) -> str:
"""Display name of the bot user. """Display name of the bot user.
Returns: Returns:
str: The display name of the bot user. Defaults to "RSSBot". str: The display name of the bot user. Defaults to "ReportBot".
""" """
return self.config["RSSBot"].get("DisplayName", "RSSBot") return self.config["ReportBot"].get("DisplayName", "ReportBot")
@property @property
def default_room_name(self) -> str: def default_room_name(self) -> str:
@ -105,7 +112,7 @@ class RSSBot:
Returns: Returns:
str: The default name of rooms created by the bot. Defaults to the display name of the bot. str: The default name of rooms created by the bot. Defaults to the display name of the bot.
""" """
return self.config["RSSBot"].get("DefaultRoomName", self.display_name) return self.config["ReportBot"].get("DefaultRoomName", self.display_name)
@property @property
def debug(self) -> bool: def debug(self) -> bool:
@ -114,32 +121,30 @@ class RSSBot:
Returns: Returns:
bool: Whether to enable debug logging. Defaults to False. bool: Whether to enable debug logging. Defaults to False.
""" """
return self.config["RSSBot"].getboolean("Debug", False) return self.config["ReportBot"].getboolean("Debug", False)
# User agent to use for HTTP requests # User agent to use for HTTP requests
USER_AGENT = ( USER_AGENT = "matrix-reportbot/dev (+https://git.private.coffee/PrivateCoffee/matrix-reportbot)"
"matrix-rssbot/dev (+https://git.private.coffee/PrivateCoffee/matrix-rssbot)"
)
@classmethod @classmethod
def from_config(cls, config: ConfigParser): def from_config(cls, config: ConfigParser):
"""Create a new RSSBot instance from a config file. """Create a new ReportBot instance from a config file.
Args: Args:
config (ConfigParser): ConfigParser instance with the bot's config. config (ConfigParser): ConfigParser instance with the bot's config.
Returns: Returns:
RSSBot: The new RSSBot instance. ReportBot: The new ReportBot instance.
""" """
# Create a new RSSBot instance # Create a new ReportBot instance
bot = cls() bot = cls()
bot.config = config bot.config = config
# Override default values # Override default values
if "RSSBot" in config: if "ReportBot" in config:
if "LogLevel" in config["RSSBot"]: if "LogLevel" in config["ReportBot"]:
bot.logger = Logger(config["RSSBot"]["LogLevel"]) bot.logger = Logger(config["ReportBot"]["LogLevel"])
# Set up the Matrix client # Set up the Matrix client
@ -151,7 +156,7 @@ class RSSBot:
bot.matrix_client.user_id = config["Matrix"].get("UserID") bot.matrix_client.user_id = config["Matrix"].get("UserID")
bot.matrix_client.device_id = config["Matrix"].get("DeviceID") bot.matrix_client.device_id = config["Matrix"].get("DeviceID")
# Return the new RSSBot instance # Return the new ReportBot instance
return bot return bot
async def _get_user_id(self) -> str: async def _get_user_id(self) -> str:
@ -200,135 +205,6 @@ class RSSBot:
return device_id return device_id
async def process_command(self, room: MatrixRoom, event: RoomMessageText):
"""Process a command. Called from the event_callback() method.
Delegates to the appropriate command handler.
Args:
room (MatrixRoom): The room the command was sent in.
event (RoomMessageText): The event containing the command.
"""
self.logger.log(
f"Received command {event.body} from {event.sender} in room {room.room_id}",
"debug",
)
if event.body.startswith("* "):
event.body = event.body[2:]
command = event.body.split()[1] if event.body.split()[1:] else None
await COMMANDS.get(command, COMMANDS[None])(room, event, self)
async def _event_callback(self, room: MatrixRoom, event: Event):
self.logger.log("Received event: " + str(event.event_id), "debug")
try:
for eventtype, callback in EVENT_CALLBACKS.items():
if isinstance(event, eventtype):
await callback(room, event, self)
except Exception as e:
self.logger.log(
f"Error in event callback for {event.__class__}: {e}", "error"
)
if self.debug:
await self.send_message(
room, f"Error: {e}\n\n```\n{traceback.format_exc()}\n```", True
)
def user_is_allowed(self, user_id: str) -> bool:
"""Check if a user is allowed to use the bot.
Args:
user_id (str): The user ID to check.
Returns:
bool: Whether the user is allowed to use the bot.
"""
return (
(
user_id in self.allowed_users
or f"*:{user_id.split(':')[1]}" in self.allowed_users
or f"@*:{user_id.split(':')[1]}" in self.allowed_users
)
if self.allowed_users
else True
)
async def event_callback(self, room: MatrixRoom, event: Event):
"""Callback for events.
Args:
room (MatrixRoom): The room the event was sent in.
event (Event): The event.
"""
if event.sender == self.matrix_client.user_id:
return
if not self.user_is_allowed(event.sender):
if len(room.users) == 2:
await self.matrix_client.room_send(
room.room_id,
"m.room.message",
{
"msgtype": "m.notice",
"body": f"You are not allowed to use this bot. Please contact {self.operator} for more information.",
},
)
return
task = asyncio.create_task(self._event_callback(room, event))
async def _response_callback(self, response: Response):
for response_type, callback in RESPONSE_CALLBACKS.items():
if isinstance(response, response_type):
await callback(response, self)
async def response_callback(self, response: Response):
task = asyncio.create_task(self._response_callback(response))
async def accept_pending_invites(self):
"""Accept all pending invites."""
assert self.matrix_client, "Matrix client not set up"
invites = self.matrix_client.invited_rooms
for invite in [k for k in invites.keys()]:
if invite in self.room_ignore_list:
self.logger.log(
f"Ignoring invite to room {invite} (room is in ignore list)",
"debug",
)
continue
self.logger.log(f"Accepting invite to room {invite}")
response = await self.matrix_client.join(invite)
if isinstance(response, JoinError):
self.logger.log(
f"Error joining room {invite}: {response.message}. Not trying again.",
"error",
)
leave_response = await self.matrix_client.room_leave(invite)
if isinstance(leave_response, RoomLeaveError):
self.logger.log(
f"Error leaving room {invite}: {leave_response.message}",
"error",
)
self.room_ignore_list.append(invite)
else:
await self.send_message(
invite, "Thank you for inviting me to your room!"
)
async def upload_file( async def upload_file(
self, self,
file: bytes, file: bytes,
@ -461,7 +337,7 @@ class RSSBot:
msgtype = msgtype if msgtype else "m.notice" if notice else "m.text" msgtype = msgtype if msgtype else "m.notice" if notice else "m.text"
if not msgtype.startswith("rssbot."): if not msgtype.startswith("reportbot."):
msgcontent = { msgcontent = {
"msgtype": msgtype, "msgtype": msgtype,
"body": message, "body": message,
@ -528,82 +404,127 @@ class RSSBot:
if state_key is None or event["state_key"] == state_key: if state_key is None or event["state_key"] == state_key:
return event return event
async def process_room(self, room): async def get_new_reports(self, last_report_id):
self.logger.log(f"Processing room {room}", "debug") # Call the Synapse admin API to get event reports since the last known one
endpoint = f"/_synapse/admin/v1/event_reports?from={last_report_id}"
state = await self.get_state_event(room, "rssbot.feeds") async with aiohttp.ClientSession() as session:
async with session.get(
if not state: f"{self.matrix_client.homeserver}{endpoint}",
feeds = [] headers={"Authorization": f"Bearer {self.matrix_client.access_token}"},
else: ) as response:
feeds = state["content"]["feeds"]
for feed in feeds:
self.logger.log(f"Processing {feed} in {room}", "debug")
feed_state = await self.get_state_event(room, "rssbot.feed_state", feed)
if feed_state:
self.logger.log(
f"Identified feed timestamp as {feed_state['content']['timestamp']}",
"debug",
)
timestamp = int(feed_state["content"]["timestamp"])
else:
timestamp = 0
try:
feed_content = feedparser.parse(feed)
new_timestamp = timestamp
for entry in feed_content.entries:
try:
entry_time_info = entry.published_parsed
except:
entry_time_info = entry.updated_parsed
entry_timestamp = int(datetime(*entry_time_info[:6]).timestamp())
self.logger.log(f"Entry timestamp identified as {entry_timestamp}")
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)
new_timestamp = max(entry_timestamp, new_timestamp)
await self.send_state_event(
room, "rssbot.feed_state", {"timestamp": new_timestamp}, feed
)
except Exception as e:
self.logger.log(f"Error processing feed at {feed}: {e}")
await self.send_message(
room,
f"Could not access or parse RSS feed at {feed}. Please ensure that you got the URL right, and that it is actually an RSS feed.",
True,
)
async def process_rooms(self):
while True:
self.logger.log("Starting to process rooms", "debug")
start_timestamp = datetime.now()
for room in self.matrix_client.rooms.values():
try: try:
await self.process_room(room) response_json = await response.json()
return (
response_json.get("event_reports", []) if response_json else []
)
except json.JSONDecodeError:
self.logger.log("Failed to decode JSON response", "error")
return []
async def get_report_details(self, report_id):
# Call the Synapse admin API to get full details on a report
endpoint = f"/_synapse/admin/v1/event_reports/{report_id}"
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.matrix_client.homeserver}{endpoint}",
headers={"Authorization": f"Bearer {self.matrix_client.access_token}"},
) as response:
try:
response_json = await response.json()
return response_json
except json.JSONDecodeError:
self.logger.log("Failed to decode JSON response", "error")
return {}
async def post_report_message(self, report_details):
# Extract relevant information from the report details
report_id = report_details.get("id")
event_id = report_details.get("event_id")
user_id = report_details.get("user_id")
room_id = report_details.get("room_id")
reason = report_details.get("reason")
content = report_details.get("event_json", {})
sender = content.get("sender")
event_type = content.get("type")
body = content.get("content", {}).get("body", "No message content")
# Format the message
message = (
f"🚨 New Event Report (ID: {report_id}) 🚨\n"
f"Event ID: {event_id}\n"
f"Reported by: {user_id}\n"
f"Room ID: {room_id}\n"
f"Reason: {reason}\n"
f"Sender: {sender}\n"
f"Event Type: {event_type}\n"
f"Message Content: {body}"
)
# Send the formatted message to the pre-configured room
await self.matrix_client.room_send(
room_id=self.room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": message,
"format": "org.matrix.custom.html",
"formatted_body": f"<pre><code>{message}</code></pre>",
},
)
async def process_reports(self):
# Task to process reports
while True:
try:
self.logger.log("Starting to process reports", "debug")
report_state = await self.get_state_event(
self.room_id, "reportbot.report_state"
)
try:
known_report = int(report_state["content"]["report"])
except:
known_report = 0
self.logger.log(f"Processing reports since: {known_report}", "debug")
try:
reports = await self.get_new_reports(known_report)
for report in reports:
report_id = report["id"]
self.logger.log(f"Processing report: {report_id}", "debug")
known_report = max(known_report, report_id)
report_details = await self.get_report_details(report_id)
await self.post_report_message(report_details)
await self.send_state_event(
self.room_id, "reportbot.report_state", {"report": known_report}
)
except Exception as e: except Exception as e:
self.logger.log( self.logger.log(f"Error processing reports: {e}")
f"Something went wrong processing room {room.room_id}: {e}", await self.send_message(
"error", self.room_id,
f"Something went wrong processing reports: {e}.",
True,
) )
end_timestamp = datetime.now() self.logger.log("Done processing reports", "debug")
await asyncio.sleep(self.loop_duration)
self.logger.log("Done processing rooms", "debug") except asyncio.CancelledError:
break
if ( except Exception as e:
time_taken := (end_timestamp - start_timestamp).seconds self.logger.log(f"Error processing reports: {e}")
) < self.loop_duration: try:
await asyncio.sleep(self.loop_duration - time_taken) await self.send_message(
self.room_id,
f"Something went wrong processing reports: {e}.",
True,
)
except Exception as e:
self.logger.log(f"Error sending error message: {e}")
async def run(self): async def run(self):
"""Start the bot.""" """Start the bot."""
@ -629,18 +550,6 @@ class RSSBot:
self.logger.log("Running initial sync...", "debug") self.logger.log("Running initial sync...", "debug")
sync = await self.matrix_client.sync(timeout=30000, full_state=True) sync = await self.matrix_client.sync(timeout=30000, full_state=True)
if isinstance(sync, SyncResponse):
await self.response_callback(sync)
else:
self.logger.log(f"Initial sync failed, aborting: {sync}", "critical")
exit(1)
# Set up callbacks
self.logger.log("Setting up callbacks...", "debug")
self.matrix_client.add_event_callback(self.event_callback, Event)
self.matrix_client.add_response_callback(self.response_callback, Response)
# Set custom name # Set custom name
@ -651,9 +560,9 @@ class RSSBot:
# Start syncing events # Start syncing events
self.logger.log("Starting sync loop...", "warning") self.logger.log("Starting sync loop...", "warning")
sync_task = self.matrix_client.sync_forever(timeout=30000, full_state=True) sync_task = self.matrix_client.sync_forever(timeout=30000, full_state=True)
feed_task = self.process_rooms() reports_task = self.process_reports()
tasks = asyncio.gather(sync_task, feed_task) tasks = asyncio.gather(sync_task, reports_task)
try: try:
await tasks await tasks

View file

@ -1,26 +0,0 @@
from nio import (
RoomMessageText,
InviteEvent,
Event,
SyncResponse,
JoinResponse,
RoomMemberEvent,
Response,
MegolmEvent,
KeysQueryResponse
)
from .sync import sync_callback
from .invite import room_invite_callback
from .message import message_callback
from .roommember import roommember_callback
RESPONSE_CALLBACKS = {
SyncResponse: sync_callback,
}
EVENT_CALLBACKS = {
InviteEvent: room_invite_callback,
RoomMessageText: message_callback,
RoomMemberEvent: roommember_callback,
}

View file

@ -1,10 +0,0 @@
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")
return
bot.logger.log(f"Received invite to room {room.room_id} - joining...")
response = await bot.matrix_client.join(room.room_id)

View file

@ -1,27 +0,0 @@
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}")
sent = datetime.fromtimestamp(event.server_timestamp / 1000)
received = datetime.now()
latency = received - sent
if event.sender == bot.matrix_client.user_id:
bot.logger.log("Message is from bot itself - ignoring", "debug")
elif event.body.startswith("!rssbot") or event.body.startswith("* !rssbot"):
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")
else:
bot.logger.log(f"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")

View file

@ -1,10 +0,0 @@
from nio import RoomMemberEvent, MatrixRoom, KeysUploadError
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")
if len(room.users) == 1:
bot.logger.log("Yes, I was abandoned - leaving...", "debug")
await bot.matrix_client.room_leave(room.room_id)
return

View file

@ -1,7 +0,0 @@
async def sync_callback(response, bot):
bot.logger.log(
f"Sync response received (next batch: {response.next_batch})", "debug")
bot.sync_response = response
await bot.accept_pending_invites()

View file

@ -1,21 +0,0 @@
from importlib import import_module
from .unknown import command_unknown
COMMANDS = {}
for command in [
"help",
"newroom",
"botinfo",
"privacy",
"addfeed",
"listfeeds",
"processfeeds",
"removefeed",
]:
function = getattr(import_module(
"." + command, "matrix_rssbot.classes.commands"), "command_" + command)
COMMANDS[command] = function
COMMANDS[None] = command_unknown

View file

@ -1,67 +0,0 @@
from nio.events.room_events import RoomMessageText
from nio import RoomPutStateError
from nio.rooms import MatrixRoom
from datetime import datetime
from urllib.request import urlopen
import feedparser
async def command_addfeed(room: MatrixRoom, event: RoomMessageText, bot):
url = event.body.split()[2]
bot.logger.log(f"Adding new feed to room {room.room_id}")
state = await bot.get_state_event(room, "rssbot.feeds")
if not state:
feeds = []
else:
feeds = state["content"]["feeds"]
feeds.append(url)
feeds = list(set(feeds))
try:
feedparser.parse(url)
except:
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,
)
try:
response1 = await bot.send_state_event(
room,
"rssbot.feed_state",
{"timestamp": int(datetime.now().timestamp())},
url,
)
if isinstance(response1, RoomPutStateError):
if response1.status_code == "M_FORBIDDEN":
await bot.send_message(
room,
"Unable to put status events into this room. Please ensure I have the required permissions, then try again.",
)
await bot.send_message(
room, "Unable to write feed state to the room. Please try again.", True
)
return
response2 = await bot.send_state_event(room, "rssbot.feeds", {"feeds": feeds})
if isinstance(response2, RoomPutStateError):
await bot.send_message(
room, "Unable to write feed list to the room. Please try again.", True
)
return
await bot.send_message(room, f"Added {url} to this room's feeds.", True)
except:
await bot.send_message(
room, "Sorry, something went wrong. Please try again.", true
)

View file

@ -1,15 +0,0 @@
from nio.events.room_events import RoomMessageText
from nio.rooms import MatrixRoom
async def command_botinfo(room: MatrixRoom, event: RoomMessageText, bot):
logging("Showing bot info...")
body = f"""
Room info:
Bot user ID: {bot.matrix_client.user_id}
Current room ID: {room.room_id}
"""
await bot.send_message(room, body, True)

View file

@ -1,17 +0,0 @@
from nio.events.room_events import RoomMessageText
from nio.rooms import MatrixRoom
async def command_help(room: MatrixRoom, event: RoomMessageText, bot):
body = """Available commands:
- !rssbot help - Show this message
- !rssbot botinfo - Show information about the bot
- !rssbot privacy - Show privacy information
- !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,16 +0,0 @@
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."
else:
message = "This room is currently bridged to the following feeds:\n\n"
for key, value in enumerate(state["content"]["feeds"]):
message += f"- {key}: {value}\n"
await bot.send_message(room, message, True)

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

@ -1,11 +0,0 @@
from nio.events.room_events import RoomMessageText
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 operator(s) of the involved Matrix homeservers\n"
await bot.send_message(room, body, True)

View file

@ -1,8 +0,0 @@
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):
bot.logger.log(f"Processing feeds for room {room.room_id}")
await bot.process_room(room)

View file

@ -1,28 +0,0 @@
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):
identifier = event.body.split()[2]
state = await bot.get_state_event(room, "rssbot.feeds")
if (not state) or (not state["content"]["feeds"]):
feeds = []
else:
feeds = state["content"]["feeds"]
if identifier.isnumeric():
try:
feed = feeds.pop(int(identifier))
except IndexError:
await bot.send_message(room, f"There is no feed with index {identifier}.")
return
else:
try:
feeds.remove(identifier)
except ValueError:
await bot.send_message(room, f"There is no bridged feed with the provided URL.")
return
await bot.send_state_event(room, "rssbot.feeds", {"feeds": feeds})

View file

@ -1,8 +0,0 @@
from nio.events.room_events import RoomMessageText
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)