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:
parent
02d7441f38
commit
8a31063bb2
28 changed files with 205 additions and 628 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ __pycache__/
|
|||
dist/
|
||||
pantalaimon.conf
|
||||
build/
|
||||
venv/
|
45
README.md
45
README.md
|
@ -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)
|
||||
[![PyPI](https://shields.private.coffee/pypi/v/matrix-rssbot)](https://pypi.org/project/matrix-rssbot/)
|
||||
[![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/matrix-rssbot)](https://pypi.org/project/matrix-rssbot/)
|
||||
[![PyPI - License](https://shields.private.coffee/pypi/l/matrix-rssbot)](https://pypi.org/project/matrix-rssbot/)
|
||||
[![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)
|
||||
[![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-reportbot)](https://pypi.org/project/matrix-reportbot/)
|
||||
[![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-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
|
||||
|
||||
```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.
|
||||
|
@ -22,42 +22,19 @@ At the very least, you need to provide the following configuration:
|
|||
[Matrix]
|
||||
Homeserver = http://your-homeserver.example.com
|
||||
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.
|
||||
|
||||
You can start the bot by running:
|
||||
|
||||
```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
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
###############################################################################
|
||||
|
||||
[RSSBot]
|
||||
[ReportBot]
|
||||
|
||||
# Some way for the user to contact you.
|
||||
# Ideally, either your personal user ID or a support room
|
||||
|
@ -24,20 +24,9 @@ Operator = Contact details not set
|
|||
#
|
||||
# Debug = 1
|
||||
|
||||
# The default room name used by the !newroom command
|
||||
# Defaults to RSSBot if not set
|
||||
#
|
||||
# DefaultRoomName = RSSBot
|
||||
|
||||
# Display name for the bot
|
||||
#
|
||||
# DisplayName = RSSBot
|
||||
|
||||
# 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"]
|
||||
# DisplayName = ReportBot
|
||||
|
||||
# Minimum level of log messages that should be printed
|
||||
# Available log levels in ascending order: trace, debug, info, warning, error, critical
|
||||
|
@ -64,6 +53,10 @@ Homeserver = https://matrix.local
|
|||
#
|
||||
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)
|
||||
# Only specify this if the bot fails to figure it out by itself
|
||||
#
|
||||
|
|
|
@ -6,20 +6,20 @@ build-backend = "hatchling.build"
|
|||
allow-direct-references = true
|
||||
|
||||
[project]
|
||||
name = "matrix-rssbot"
|
||||
version = "0.1.1"
|
||||
name = "matrix-reportbot"
|
||||
version = "0.0.1"
|
||||
|
||||
authors = [
|
||||
{ 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"
|
||||
license = { file="LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
|
||||
packages = [
|
||||
"src/matrix_rssbot"
|
||||
"src/matrix_reportbot"
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
|
@ -33,15 +33,14 @@ dependencies = [
|
|||
"pantalaimon",
|
||||
"setuptools",
|
||||
"markdown2",
|
||||
"feedparser",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://git.private.coffee/PrivateCoffee/matrix-rssbot"
|
||||
"Bug Tracker" = "https://git.private.coffee/PrivateCoffee/matrix-rssbot/issues"
|
||||
"Homepage" = "https://git.private.coffee/PrivateCoffee/matrix-reportbot"
|
||||
"Bug Tracker" = "https://git.private.coffee/PrivateCoffee/matrix-reportbot/issues"
|
||||
|
||||
[project.scripts]
|
||||
rssbot = "matrix_rssbot.__main__:main"
|
||||
reportbot = "matrix_reportbot.__main__:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/matrix_rssbot"]
|
||||
packages = ["src/matrix_reportbot"]
|
15
reportbot-pantalaimon.service
Normal file
15
reportbot-pantalaimon.service
Normal 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
15
reportbot.service
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
from .classes.bot import RSSBot
|
||||
from .classes.bot import ReportBot
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from configparser import ConfigParser
|
||||
|
@ -12,7 +12,7 @@ def sigterm_handler(_signo, _stack_frame):
|
|||
|
||||
def get_version():
|
||||
try:
|
||||
package_version = pkg_resources.get_distribution("matrix_rssbot").version
|
||||
package_version = pkg_resources.get_distribution("matrix_reportbot").version
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return None
|
||||
return package_version
|
||||
|
@ -40,7 +40,7 @@ def main():
|
|||
config.read(args.config)
|
||||
|
||||
# Create bot
|
||||
bot = RSSBot.from_config(config)
|
||||
bot = ReportBot.from_config(config)
|
||||
|
||||
# Listen for SIGTERM
|
||||
signal.signal(signal.SIGTERM, sigterm_handler)
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
import functools
|
||||
import aiohttp
|
||||
|
||||
from nio import (
|
||||
AsyncClient,
|
||||
|
@ -50,14 +51,11 @@ import sys
|
|||
import traceback
|
||||
|
||||
import markdown2
|
||||
import feedparser
|
||||
|
||||
from .logging import Logger
|
||||
from .callbacks import RESPONSE_CALLBACKS, EVENT_CALLBACKS
|
||||
from .commands import COMMANDS
|
||||
|
||||
|
||||
class RSSBot:
|
||||
class ReportBot:
|
||||
# Default values
|
||||
matrix_client: Optional[AsyncClient] = None
|
||||
sync_token: Optional[str] = None
|
||||
|
@ -75,7 +73,7 @@ class RSSBot:
|
|||
|
||||
@property
|
||||
def loop_duration(self) -> int:
|
||||
return self.config["RSSBot"].getint("LoopDuration", 300)
|
||||
return self.config["ReportBot"].getint("LoopDuration", 300)
|
||||
|
||||
@property
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
return json.loads(self.config["RSSBot"]["AllowedUsers"])
|
||||
return json.loads(self.config["ReportBot"]["AllowedUsers"])
|
||||
except:
|
||||
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
|
||||
def display_name(self) -> str:
|
||||
"""Display name of the bot user.
|
||||
|
||||
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
|
||||
def default_room_name(self) -> str:
|
||||
|
@ -105,7 +112,7 @@ class RSSBot:
|
|||
Returns:
|
||||
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
|
||||
def debug(self) -> bool:
|
||||
|
@ -114,32 +121,30 @@ class RSSBot:
|
|||
Returns:
|
||||
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 = (
|
||||
"matrix-rssbot/dev (+https://git.private.coffee/PrivateCoffee/matrix-rssbot)"
|
||||
)
|
||||
USER_AGENT = "matrix-reportbot/dev (+https://git.private.coffee/PrivateCoffee/matrix-reportbot)"
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
config (ConfigParser): ConfigParser instance with the bot's config.
|
||||
|
||||
Returns:
|
||||
RSSBot: The new RSSBot instance.
|
||||
ReportBot: The new ReportBot instance.
|
||||
"""
|
||||
|
||||
# Create a new RSSBot instance
|
||||
# Create a new ReportBot instance
|
||||
bot = cls()
|
||||
bot.config = config
|
||||
|
||||
# Override default values
|
||||
if "RSSBot" in config:
|
||||
if "LogLevel" in config["RSSBot"]:
|
||||
bot.logger = Logger(config["RSSBot"]["LogLevel"])
|
||||
if "ReportBot" in config:
|
||||
if "LogLevel" in config["ReportBot"]:
|
||||
bot.logger = Logger(config["ReportBot"]["LogLevel"])
|
||||
|
||||
# Set up the Matrix client
|
||||
|
||||
|
@ -151,7 +156,7 @@ class RSSBot:
|
|||
bot.matrix_client.user_id = config["Matrix"].get("UserID")
|
||||
bot.matrix_client.device_id = config["Matrix"].get("DeviceID")
|
||||
|
||||
# Return the new RSSBot instance
|
||||
# Return the new ReportBot instance
|
||||
return bot
|
||||
|
||||
async def _get_user_id(self) -> str:
|
||||
|
@ -200,135 +205,6 @@ class RSSBot:
|
|||
|
||||
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(
|
||||
self,
|
||||
file: bytes,
|
||||
|
@ -461,7 +337,7 @@ class RSSBot:
|
|||
|
||||
msgtype = msgtype if msgtype else "m.notice" if notice else "m.text"
|
||||
|
||||
if not msgtype.startswith("rssbot."):
|
||||
if not msgtype.startswith("reportbot."):
|
||||
msgcontent = {
|
||||
"msgtype": msgtype,
|
||||
"body": message,
|
||||
|
@ -528,82 +404,127 @@ class RSSBot:
|
|||
if state_key is None or event["state_key"] == state_key:
|
||||
return event
|
||||
|
||||
async def process_room(self, room):
|
||||
self.logger.log(f"Processing room {room}", "debug")
|
||||
|
||||
state = await self.get_state_event(room, "rssbot.feeds")
|
||||
|
||||
if not state:
|
||||
feeds = []
|
||||
else:
|
||||
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():
|
||||
async def get_new_reports(self, last_report_id):
|
||||
# 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}"
|
||||
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:
|
||||
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:
|
||||
self.logger.log(
|
||||
f"Something went wrong processing room {room.room_id}: {e}",
|
||||
"error",
|
||||
self.logger.log(f"Error processing reports: {e}")
|
||||
await self.send_message(
|
||||
self.room_id,
|
||||
f"Something went wrong processing reports: {e}.",
|
||||
True,
|
||||
)
|
||||
|
||||
end_timestamp = datetime.now()
|
||||
|
||||
self.logger.log("Done processing rooms", "debug")
|
||||
|
||||
if (
|
||||
time_taken := (end_timestamp - start_timestamp).seconds
|
||||
) < self.loop_duration:
|
||||
await asyncio.sleep(self.loop_duration - time_taken)
|
||||
self.logger.log("Done processing reports", "debug")
|
||||
await asyncio.sleep(self.loop_duration)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error processing reports: {e}")
|
||||
try:
|
||||
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):
|
||||
"""Start the bot."""
|
||||
|
@ -629,18 +550,6 @@ class RSSBot:
|
|||
self.logger.log("Running initial sync...", "debug")
|
||||
|
||||
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
|
||||
|
||||
|
@ -651,9 +560,9 @@ class RSSBot:
|
|||
# Start syncing events
|
||||
self.logger.log("Starting sync loop...", "warning")
|
||||
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:
|
||||
await tasks
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
|
@ -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")
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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})
|
|
@ -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)
|
Loading…
Reference in a new issue