diff --git a/base-config.yaml b/base-config.yaml index e7ada45..ca97d31 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -15,9 +15,9 @@ notification_template: "New post in $feed_title: [$title]($link)" # Should non-admin users be allowed to set regex filters to drop posts from feeds? # Regexes are dangerous and may lead to DoS attacks. allow_filter: false -# Per-subscription match filter (whitelist): set with `!rss set-filter `. +# Global match filter (whitelist): applies to ALL feeds in this instance. # A post is ONLY sent when the regex matches the title OR the (HTML-stripped) summary. -# Empty = fall back to the default below. +# Set it live with `!rss set-filter `. Empty = send everything. match_filter: "(?i)\\b(?:gvb|ebs|connexxion|cxx|transdev|keolis|ret|gvu|bus|metro|tram|ov)\\b|trein(?:en|verkeer|stel(?:len)?|staking|ongeluk)?|\\bov-staking|staking" # Users who can bypass room permission checks admins: diff --git a/buzzrss/bot.py b/buzzrss/bot.py index cefba65..947d1ee 100644 --- a/buzzrss/bot.py +++ b/buzzrss/bot.py @@ -126,7 +126,7 @@ class RSSBot(Plugin): self.log.exception("Fatal error while polling feeds") async def _send(self, feed: Feed, entry: Entry, sub: Subscription) -> EventID | None: - match_filter = sub.match_filter or self.config["match_filter"] + match_filter = self.config["match_filter"] if match_filter: haystack = f"{entry.title}\n{html.unescape(re.sub(r'<[^>]+>', '', entry.summary))}" if not re.search(match_filter, haystack): @@ -407,37 +407,30 @@ class RSSBot(Plugin): await evt.reply(f"Subscribed to {feed_info}") @rss.subcommand( - "set-filter", aliases=("f", "filter"), help="Set match filter (whitelist regex) for a specific feed." + "set-filter", aliases=("f", "filter"), help="Set the global match filter (whitelist regex) for all feeds." ) - @command.argument("feed_id", "feed ID", parser=int) @command.argument("match_filter", "match filter", pass_raw=True) - async def set_filter(self, evt: MessageEvent, feed_id: int, match_filter: str) -> None: + async def set_filter(self, evt: MessageEvent, match_filter: str) -> None: if evt.sender in self.config["admins"]: pass elif not self.config["allow_filter"]: - await evt.reply("This bot does not allow non-admin users to set title filters") + await evt.reply("This bot does not allow non-admin users to set the match filter") return elif not await self.can_manage(evt): return - sub, feed = await self.dbm.get_subscription(feed_id, evt.room_id) - if not sub: - await evt.reply("This room is not subscribed to that feed") - return try: re.compile(match_filter) except re.error: await evt.reply(f"Filter is not a valid regular expression") return - await self.dbm.update_match_filter(feed.id, evt.room_id, match_filter) + self.config["match_filter"] = match_filter + self.config.save() if match_filter: await evt.reply( - f"Feed {feed_id} will now only send posts matching: `{match_filter}`" + f"Global filter updated: only posts matching `{match_filter}` will be sent" ) else: - await evt.reply( - f"Removed match filter from feed {feed_id} " - f"(falling back to default: `{self.config['match_filter']}`)" - ) + await evt.reply("Global filter removed (all posts will be sent)") @rss.subcommand( "unsubscribe", aliases=("u", "unsub"), help="Unsubscribe this room from a feed." @@ -512,13 +505,11 @@ class RSSBot(Plugin): await evt.reply(f"Updates for feed ID {feed.id} will now be sent as `{send_type}`") @staticmethod - def _format_subscription(feed: Feed, subscriber: str, match_filter: str) -> str: + def _format_subscription(feed: Feed, subscriber: str) -> str: msg = ( f"* {feed.id} - [{feed.title}]({feed.url}) " f"(subscribed by [{subscriber}](https://matrix.to/#/{subscriber}))" ) - if match_filter: - msg += f" (only posts matching: {match_filter})" if feed.error_count > 1: msg += f" \n ⚠️ The last {feed.error_count} attempts to fetch the feed have failed!" return msg @@ -530,15 +521,22 @@ class RSSBot(Plugin): ) async def command_subscriptions(self, evt: MessageEvent) -> None: subscriptions = await self.dbm.get_feeds_by_room(evt.room_id) + match_filter = self.config["match_filter"] if len(subscriptions) == 0: await evt.reply("There are no RSS subscriptions in this room") return + filter_line = ( + f"**Global filter:** `{match_filter}`" + if match_filter + else "**Global filter:** none (all posts are sent)" + ) await evt.reply( "**Subscriptions in this room:**\n\n" + "\n".join( - self._format_subscription(feed, subscriber, match_filter) - for feed, subscriber, match_filter in subscriptions + self._format_subscription(feed, subscriber) + for feed, subscriber in subscriptions ) + + f"\n\n{filter_line}" ) @event.on(EventType.ROOM_TOMBSTONE) diff --git a/buzzrss/db.py b/buzzrss/db.py index ba71a0b..52bd08f 100644 --- a/buzzrss/db.py +++ b/buzzrss/db.py @@ -33,7 +33,6 @@ class Subscription: user_id: UserID notification_template: Template send_notice: bool - match_filter: str @classmethod def from_row(cls, row: Record | None) -> Subscription | None: @@ -46,14 +45,12 @@ class Subscription: return None send_notice = bool(row["send_notice"]) tpl = Template(row["notification_template"]) - match_filter = row["match_filter"] return cls( feed_id=feed_id, room_id=room_id, user_id=user_id, notification_template=tpl, send_notice=send_notice, - match_filter=match_filter, ) @@ -79,7 +76,6 @@ class Feed: data.pop("user_id", None) data.pop("send_notice", None) data.pop("notification_template", None) - data.pop("match_filter", None) return cls(**data, subscriptions=[]) @@ -123,7 +119,7 @@ class DBManager: async def get_feeds(self) -> list[Feed]: q = """ SELECT id, url, title, subtitle, link, next_retry, error_count, - room_id, user_id, notification_template, send_notice, match_filter + room_id, user_id, notification_template, send_notice FROM feed INNER JOIN subscription ON feed.id = subscription.feed_id """ rows = await self.db.fetch(q) @@ -136,13 +132,13 @@ class DBManager: feed.subscriptions.append(Subscription.from_row(row)) return list(feeds.values()) - async def get_feeds_by_room(self, room_id: RoomID) -> list[tuple[Feed, UserID, str]]: + async def get_feeds_by_room(self, room_id: RoomID) -> list[tuple[Feed, UserID]]: q = """ - SELECT id, url, title, subtitle, link, next_retry, error_count, user_id, match_filter FROM feed + SELECT id, url, title, subtitle, link, next_retry, error_count, user_id FROM feed INNER JOIN subscription ON feed.id = subscription.feed_id AND subscription.room_id = $1 """ rows = await self.db.fetch(q, room_id) - return [(Feed.from_row(row), row["user_id"], row["match_filter"]) for row in rows] + return [(Feed.from_row(row), row["user_id"]) for row in rows] async def filter_entries(self, feed_id: int, entries: dict[str, Entry]) -> None: entry_keys = list(entries.keys()) @@ -185,7 +181,7 @@ class DBManager: ) -> tuple[Subscription | None, Feed | None]: q = """ SELECT id, url, title, subtitle, link, next_retry, error_count, - room_id, user_id, notification_template, send_notice, match_filter + room_id, user_id, notification_template, send_notice FROM feed LEFT JOIN subscription ON feed.id = subscription.feed_id AND room_id = $2 WHERE feed.id = $1 """ @@ -235,9 +231,3 @@ class DBManager: async def set_send_notice(self, feed_id: int, room_id: RoomID, send_notice: bool) -> None: q = "UPDATE subscription SET send_notice=$3 WHERE feed_id=$1 AND room_id=$2" await self.db.execute(q, feed_id, room_id, send_notice) - - async def update_match_filter( - self, feed_id: int, room_id: RoomID, match_filter: str - ) -> None: - q = "UPDATE subscription SET match_filter=$3 WHERE feed_id=$1 AND room_id=$2" - await self.db.execute(q, feed_id, room_id, match_filter) diff --git a/buzzrss/migrations.py b/buzzrss/migrations.py index 7691ea7..2d9f3b6 100644 --- a/buzzrss/migrations.py +++ b/buzzrss/migrations.py @@ -89,3 +89,8 @@ async def upgrade_v5(conn: Connection) -> None: @upgrade_table.register(description="Rename title exclude filter to match filter (whitelist mode)") async def upgrade_v6(conn: Connection) -> None: await conn.execute("ALTER TABLE subscription RENAME COLUMN title_exclude_filter TO match_filter") + + +@upgrade_table.register(description="Remove per-subscription filter (single global config filter)") +async def upgrade_v7(conn: Connection) -> None: + await conn.execute("ALTER TABLE subscription DROP COLUMN IF EXISTS match_filter") diff --git a/maubot.yaml b/maubot.yaml index d6e9fa6..47521c1 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,5 +1,5 @@ id: nl.eddydevink.rss -version: 1.0.1 +version: 1.1.0 modules: - buzzrss main_class: RSSBot