v1.1.0: enkel globaal filter (config) in plaats van per-subscription
- match_filter is nu instance-config (geldt voor ALLE feeds), niet per subscription: !rss set-filter <regex> zet het globale filter en slaat config op; !rss subscriptions toont het globale filter - v7-migratie: per-subscription kolom match_filter verwijderd - filter scant titel + HTML-ontdaan summary
This commit is contained in:
parent
0628776469
commit
676f999cce
5 changed files with 31 additions and 38 deletions
|
|
@ -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?
|
# Should non-admin users be allowed to set regex filters to drop posts from feeds?
|
||||||
# Regexes are dangerous and may lead to DoS attacks.
|
# Regexes are dangerous and may lead to DoS attacks.
|
||||||
allow_filter: false
|
allow_filter: false
|
||||||
# Per-subscription match filter (whitelist): set with `!rss set-filter <feed-id> <regex>`.
|
# 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.
|
# 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 <regex>`. 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"
|
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
|
# Users who can bypass room permission checks
|
||||||
admins:
|
admins:
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ class RSSBot(Plugin):
|
||||||
self.log.exception("Fatal error while polling feeds")
|
self.log.exception("Fatal error while polling feeds")
|
||||||
|
|
||||||
async def _send(self, feed: Feed, entry: Entry, sub: Subscription) -> EventID | None:
|
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:
|
if match_filter:
|
||||||
haystack = f"{entry.title}\n{html.unescape(re.sub(r'<[^>]+>', '', entry.summary))}"
|
haystack = f"{entry.title}\n{html.unescape(re.sub(r'<[^>]+>', '', entry.summary))}"
|
||||||
if not re.search(match_filter, haystack):
|
if not re.search(match_filter, haystack):
|
||||||
|
|
@ -407,37 +407,30 @@ class RSSBot(Plugin):
|
||||||
await evt.reply(f"Subscribed to {feed_info}")
|
await evt.reply(f"Subscribed to {feed_info}")
|
||||||
|
|
||||||
@rss.subcommand(
|
@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)
|
@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"]:
|
if evt.sender in self.config["admins"]:
|
||||||
pass
|
pass
|
||||||
elif not self.config["allow_filter"]:
|
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
|
return
|
||||||
elif not await self.can_manage(evt):
|
elif not await self.can_manage(evt):
|
||||||
return
|
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:
|
try:
|
||||||
re.compile(match_filter)
|
re.compile(match_filter)
|
||||||
except re.error:
|
except re.error:
|
||||||
await evt.reply(f"Filter is not a valid regular expression")
|
await evt.reply(f"Filter is not a valid regular expression")
|
||||||
return
|
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:
|
if match_filter:
|
||||||
await evt.reply(
|
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:
|
else:
|
||||||
await evt.reply(
|
await evt.reply("Global filter removed (all posts will be sent)")
|
||||||
f"Removed match filter from feed {feed_id} "
|
|
||||||
f"(falling back to default: `{self.config['match_filter']}`)"
|
|
||||||
)
|
|
||||||
|
|
||||||
@rss.subcommand(
|
@rss.subcommand(
|
||||||
"unsubscribe", aliases=("u", "unsub"), help="Unsubscribe this room from a feed."
|
"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}`")
|
await evt.reply(f"Updates for feed ID {feed.id} will now be sent as `{send_type}`")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_subscription(feed: Feed, subscriber: str, match_filter: str) -> str:
|
def _format_subscription(feed: Feed, subscriber: str) -> str:
|
||||||
msg = (
|
msg = (
|
||||||
f"* {feed.id} - [{feed.title}]({feed.url}) "
|
f"* {feed.id} - [{feed.title}]({feed.url}) "
|
||||||
f"(subscribed by [{subscriber}](https://matrix.to/#/{subscriber}))"
|
f"(subscribed by [{subscriber}](https://matrix.to/#/{subscriber}))"
|
||||||
)
|
)
|
||||||
if match_filter:
|
|
||||||
msg += f" (only posts matching: {match_filter})"
|
|
||||||
if feed.error_count > 1:
|
if feed.error_count > 1:
|
||||||
msg += f" \n ⚠️ The last {feed.error_count} attempts to fetch the feed have failed!"
|
msg += f" \n ⚠️ The last {feed.error_count} attempts to fetch the feed have failed!"
|
||||||
return msg
|
return msg
|
||||||
|
|
@ -530,15 +521,22 @@ class RSSBot(Plugin):
|
||||||
)
|
)
|
||||||
async def command_subscriptions(self, evt: MessageEvent) -> None:
|
async def command_subscriptions(self, evt: MessageEvent) -> None:
|
||||||
subscriptions = await self.dbm.get_feeds_by_room(evt.room_id)
|
subscriptions = await self.dbm.get_feeds_by_room(evt.room_id)
|
||||||
|
match_filter = self.config["match_filter"]
|
||||||
if len(subscriptions) == 0:
|
if len(subscriptions) == 0:
|
||||||
await evt.reply("There are no RSS subscriptions in this room")
|
await evt.reply("There are no RSS subscriptions in this room")
|
||||||
return
|
return
|
||||||
|
filter_line = (
|
||||||
|
f"**Global filter:** `{match_filter}`"
|
||||||
|
if match_filter
|
||||||
|
else "**Global filter:** none (all posts are sent)"
|
||||||
|
)
|
||||||
await evt.reply(
|
await evt.reply(
|
||||||
"**Subscriptions in this room:**\n\n"
|
"**Subscriptions in this room:**\n\n"
|
||||||
+ "\n".join(
|
+ "\n".join(
|
||||||
self._format_subscription(feed, subscriber, match_filter)
|
self._format_subscription(feed, subscriber)
|
||||||
for feed, subscriber, match_filter in subscriptions
|
for feed, subscriber in subscriptions
|
||||||
)
|
)
|
||||||
|
+ f"\n\n{filter_line}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@event.on(EventType.ROOM_TOMBSTONE)
|
@event.on(EventType.ROOM_TOMBSTONE)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ class Subscription:
|
||||||
user_id: UserID
|
user_id: UserID
|
||||||
notification_template: Template
|
notification_template: Template
|
||||||
send_notice: bool
|
send_notice: bool
|
||||||
match_filter: str
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Record | None) -> Subscription | None:
|
def from_row(cls, row: Record | None) -> Subscription | None:
|
||||||
|
|
@ -46,14 +45,12 @@ class Subscription:
|
||||||
return None
|
return None
|
||||||
send_notice = bool(row["send_notice"])
|
send_notice = bool(row["send_notice"])
|
||||||
tpl = Template(row["notification_template"])
|
tpl = Template(row["notification_template"])
|
||||||
match_filter = row["match_filter"]
|
|
||||||
return cls(
|
return cls(
|
||||||
feed_id=feed_id,
|
feed_id=feed_id,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
notification_template=tpl,
|
notification_template=tpl,
|
||||||
send_notice=send_notice,
|
send_notice=send_notice,
|
||||||
match_filter=match_filter,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,7 +76,6 @@ class Feed:
|
||||||
data.pop("user_id", None)
|
data.pop("user_id", None)
|
||||||
data.pop("send_notice", None)
|
data.pop("send_notice", None)
|
||||||
data.pop("notification_template", None)
|
data.pop("notification_template", None)
|
||||||
data.pop("match_filter", None)
|
|
||||||
return cls(**data, subscriptions=[])
|
return cls(**data, subscriptions=[])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,7 +119,7 @@ class DBManager:
|
||||||
async def get_feeds(self) -> list[Feed]:
|
async def get_feeds(self) -> list[Feed]:
|
||||||
q = """
|
q = """
|
||||||
SELECT id, url, title, subtitle, link, next_retry, error_count,
|
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
|
FROM feed INNER JOIN subscription ON feed.id = subscription.feed_id
|
||||||
"""
|
"""
|
||||||
rows = await self.db.fetch(q)
|
rows = await self.db.fetch(q)
|
||||||
|
|
@ -136,13 +132,13 @@ class DBManager:
|
||||||
feed.subscriptions.append(Subscription.from_row(row))
|
feed.subscriptions.append(Subscription.from_row(row))
|
||||||
return list(feeds.values())
|
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 = """
|
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
|
INNER JOIN subscription ON feed.id = subscription.feed_id AND subscription.room_id = $1
|
||||||
"""
|
"""
|
||||||
rows = await self.db.fetch(q, room_id)
|
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:
|
async def filter_entries(self, feed_id: int, entries: dict[str, Entry]) -> None:
|
||||||
entry_keys = list(entries.keys())
|
entry_keys = list(entries.keys())
|
||||||
|
|
@ -185,7 +181,7 @@ class DBManager:
|
||||||
) -> tuple[Subscription | None, Feed | None]:
|
) -> tuple[Subscription | None, Feed | None]:
|
||||||
q = """
|
q = """
|
||||||
SELECT id, url, title, subtitle, link, next_retry, error_count,
|
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
|
FROM feed LEFT JOIN subscription ON feed.id = subscription.feed_id AND room_id = $2
|
||||||
WHERE feed.id = $1
|
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:
|
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"
|
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)
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -89,3 +89,8 @@ async def upgrade_v5(conn: Connection) -> None:
|
||||||
@upgrade_table.register(description="Rename title exclude filter to match filter (whitelist mode)")
|
@upgrade_table.register(description="Rename title exclude filter to match filter (whitelist mode)")
|
||||||
async def upgrade_v6(conn: Connection) -> None:
|
async def upgrade_v6(conn: Connection) -> None:
|
||||||
await conn.execute("ALTER TABLE subscription RENAME COLUMN title_exclude_filter TO match_filter")
|
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")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
id: nl.eddydevink.rss
|
id: nl.eddydevink.rss
|
||||||
version: 1.0.1
|
version: 1.1.0
|
||||||
modules:
|
modules:
|
||||||
- buzzrss
|
- buzzrss
|
||||||
main_class: RSSBot
|
main_class: RSSBot
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue