diff --git a/rss/bot.py b/rss/bot.py index 945903b..3be4191 100644 --- a/rss/bot.py +++ b/rss/bot.py @@ -22,6 +22,7 @@ from time import mktime, time import asyncio import hashlib import html +import re import aiohttp import attr @@ -105,7 +106,12 @@ class RSSBot(Plugin): except Exception: self.log.exception("Fatal error while polling feeds") - async def _send(self, feed: Feed, entry: Entry, sub: Subscription) -> EventID: + async def _send(self, feed: Feed, entry: Entry, sub: Subscription) -> EventID | None: + title_exclude_filter = sub.title_exclude_filter + if title_exclude_filter: + if re.search(title_exclude_filter, entry.title): + return None + message = sub.notification_template.safe_substitute( { "feed_url": feed.url, @@ -373,6 +379,31 @@ class RSSBot(Plugin): ) await evt.reply(f"Subscribed to {feed_info}") + @rss.subcommand( + "set-filter", aliases=("f", "filter"), help="Set title exclude filter for a specific feed." + ) + @command.argument("feed_id", "feed ID", parser=int) + @command.argument("title_exclude_filter", "title exclude filter", pass_raw=True) + async def set_filter(self, evt: MessageEvent, feed_id: int, title_exclude_filter: str) -> None: + if 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(title_exclude_filter) + except re.error: + await evt.reply(f"Filter is not a valid regular expression") + return + await self.dbm.update_title_filter(feed.id, evt.room_id, title_exclude_filter) + if title_exclude_filter: + await evt.reply( + f"Feed {feed_id} will now exclude titles matching: {title_exclude_filter}" + ) + else: + await evt.reply(f"Removed title exclude filter from feed {feed_id}") + @rss.subcommand( "unsubscribe", aliases=("u", "unsub"), help="Unsubscribe this room from a feed." ) @@ -446,11 +477,13 @@ 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) -> str: + def _format_subscription(feed: Feed, subscriber: str, title_exclude_filter: str) -> str: msg = ( f"* {feed.id} - [{feed.title}]({feed.url}) " f"(subscribed by [{subscriber}](https://matrix.to/#/{subscriber}))" ) + if title_exclude_filter: + msg += f" (excludes titles matching: {title_exclude_filter})" if feed.error_count > 1: msg += f" \n ⚠️ The last {feed.error_count} attempts to fetch the feed have failed!" return msg @@ -468,7 +501,8 @@ class RSSBot(Plugin): await evt.reply( "**Subscriptions in this room:**\n\n" + "\n".join( - self._format_subscription(feed, subscriber) for feed, subscriber in subscriptions + self._format_subscription(feed, subscriber, title_exclude_filter) + for feed, subscriber, title_exclude_filter in subscriptions ) ) diff --git a/rss/db.py b/rss/db.py index e6faa88..05be16d 100644 --- a/rss/db.py +++ b/rss/db.py @@ -39,6 +39,7 @@ class Subscription: user_id: UserID notification_template: Template send_notice: bool + title_exclude_filter: str @classmethod def from_row(cls, row: Record | None) -> Subscription | None: @@ -51,12 +52,14 @@ class Subscription: return None send_notice = bool(row["send_notice"]) tpl = Template(row["notification_template"]) + exclude_filter = row["title_exclude_filter"] return cls( feed_id=feed_id, room_id=room_id, user_id=user_id, notification_template=tpl, send_notice=send_notice, + title_exclude_filter=exclude_filter, ) @@ -82,6 +85,7 @@ class Feed: data.pop("user_id", None) data.pop("send_notice", None) data.pop("notification_template", None) + data.pop("title_exclude_filter", None) return cls(**data, subscriptions=[]) @@ -121,7 +125,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 + room_id, user_id, notification_template, send_notice, title_exclude_filter FROM feed INNER JOIN subscription ON feed.id = subscription.feed_id """ rows = await self.db.fetch(q) @@ -134,13 +138,14 @@ 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]]: + async def get_feeds_by_room(self, room_id: RoomID) -> list[tuple[Feed, UserID, str]]: q = """ - SELECT id, url, title, subtitle, link, next_retry, error_count, user_id FROM feed + SELECT id, url, title, subtitle, link, next_retry, error_count, user_id, title_exclude_filter FROM feed INNER JOIN subscription ON feed.id = subscription.feed_id AND subscription.room_id = $1 + ORDER BY id """ rows = await self.db.fetch(q, room_id) - return [(Feed.from_row(row), row["user_id"]) for row in rows] + return [(Feed.from_row(row), row["user_id"], row["title_exclude_filter"]) for row in rows] async def get_entries(self, feed_id: int) -> list[Entry]: q = "SELECT feed_id, id, date, title, summary, link FROM entry WHERE feed_id = $1" @@ -173,7 +178,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 + room_id, user_id, notification_template, send_notice, title_exclude_filter FROM feed LEFT JOIN subscription ON feed.id = subscription.feed_id AND room_id = $2 WHERE feed.id = $1 """ @@ -238,3 +243,9 @@ 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_title_filter( + self, feed_id: int, room_id: RoomID, title_exclude_filter: str + ) -> None: + q = "UPDATE subscription SET title_exclude_filter=$3 WHERE feed_id=$1 AND room_id=$2" + await self.db.execute(q, feed_id, room_id, title_exclude_filter) diff --git a/rss/migrations.py b/rss/migrations.py index 689f784..ac77bf4 100644 --- a/rss/migrations.py +++ b/rss/migrations.py @@ -18,7 +18,7 @@ from mautrix.util.async_db import Connection, Scheme, UpgradeTable upgrade_table = UpgradeTable() -@upgrade_table.register(description="Latest revision", upgrades_to=3) +@upgrade_table.register(description="Latest revision", upgrades_to=4) async def upgrade_latest(conn: Connection, scheme: Scheme) -> None: gen = "GENERATED ALWAYS AS IDENTITY" if scheme != Scheme.SQLITE else "" await conn.execute( @@ -44,6 +44,7 @@ async def upgrade_latest(conn: Connection, scheme: Scheme) -> None: notification_template TEXT, send_notice BOOLEAN DEFAULT true, + title_exclude_filter TEXT DEFAULT '' PRIMARY KEY (feed_id, room_id), FOREIGN KEY (feed_id) REFERENCES feed (id) @@ -72,3 +73,8 @@ async def upgrade_v2(conn: Connection) -> None: async def upgrade_v3(conn: Connection) -> None: await conn.execute("ALTER TABLE feed ADD COLUMN next_retry BIGINT DEFAULT 0") await conn.execute("ALTER TABLE feed ADD COLUMN error_count BIGINT DEFAULT 0") + + +@upgrade_table.register(description="Add title exclude filter to subscriptions") +async def upgrade_v4(conn: Connection) -> None: + await conn.execute("ALTER TABLE subscription ADD COLUMN title_exclude_filter TEXT DEFAULT ''")