Make blacklist override whitelist
All checks were successful
Test and build Docker image / docker (push) Successful in 30s
All checks were successful
Test and build Docker image / docker (push) Successful in 30s
Change filter evaluation so blacklist matches take precedence over whitelist matches. Updated evaluator logic to skip entries when blacklist and whitelist both match, adjusted related branches to reflect the new decision flow, and updated a feeds.py comment to clarify the combined decision. Also updated blacklist/whitelist templates copy to reflect the new precedence and adjusted tests to expect blacklist-wins behavior.
This commit is contained in:
parent
dc17850475
commit
d85bc16904
7 changed files with 24 additions and 34 deletions
|
|
@ -732,7 +732,7 @@ def send_to_discord(reader: Reader | None = None, feed: Feed | None = None, *, d
|
|||
else:
|
||||
logger.warning("No entry link found for feed %s, falling back to regular processing", entry.feed.url)
|
||||
|
||||
# Send the entry to Discord as it is not blacklisted or feed has a whitelist.
|
||||
# Send the entry to Discord because the combined blacklist/whitelist decision allowed it.
|
||||
execute_webhook(webhook, entry, reader=effective_reader)
|
||||
|
||||
# If we only want to send one entry, we will break the loop. This is used when testing this function.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ def evaluate_entry_filters(
|
|||
) -> EntryFilterDecision:
|
||||
"""Evaluate one entry against blacklist and whitelist settings.
|
||||
|
||||
Whitelist matches take precedence over blacklist matches.
|
||||
Blacklist matches take precedence over whitelist matches.
|
||||
|
||||
Args:
|
||||
entry: The entry to evaluate.
|
||||
|
|
@ -140,10 +140,20 @@ def evaluate_entry_filters(
|
|||
has_blacklist_filters: bool = has_filter_values(normalized_blacklist_values)
|
||||
has_whitelist_filters: bool = has_filter_values(normalized_whitelist_values)
|
||||
|
||||
if whitelist_match and blacklist_match:
|
||||
if blacklist_match and whitelist_match:
|
||||
return EntryFilterDecision(
|
||||
should_send=True,
|
||||
reason=f"Sent because {whitelist_match.description}; whitelist overrides blacklist.",
|
||||
should_send=False,
|
||||
reason=f"Skipped because {blacklist_match.description}; blacklist overrides whitelist.",
|
||||
blacklist_match=blacklist_match,
|
||||
whitelist_match=whitelist_match,
|
||||
has_blacklist_filters=has_blacklist_filters,
|
||||
has_whitelist_filters=has_whitelist_filters,
|
||||
)
|
||||
|
||||
if blacklist_match:
|
||||
return EntryFilterDecision(
|
||||
should_send=False,
|
||||
reason=f"Skipped because {blacklist_match.description}.",
|
||||
blacklist_match=blacklist_match,
|
||||
whitelist_match=whitelist_match,
|
||||
has_blacklist_filters=has_blacklist_filters,
|
||||
|
|
@ -160,16 +170,6 @@ def evaluate_entry_filters(
|
|||
has_whitelist_filters=has_whitelist_filters,
|
||||
)
|
||||
|
||||
if has_whitelist_filters and blacklist_match:
|
||||
return EntryFilterDecision(
|
||||
should_send=False,
|
||||
reason=f"Skipped because {blacklist_match.description} and no whitelist rule matched.",
|
||||
blacklist_match=blacklist_match,
|
||||
whitelist_match=whitelist_match,
|
||||
has_blacklist_filters=has_blacklist_filters,
|
||||
has_whitelist_filters=has_whitelist_filters,
|
||||
)
|
||||
|
||||
if has_whitelist_filters:
|
||||
return EntryFilterDecision(
|
||||
should_send=False,
|
||||
|
|
@ -180,16 +180,6 @@ def evaluate_entry_filters(
|
|||
has_whitelist_filters=has_whitelist_filters,
|
||||
)
|
||||
|
||||
if blacklist_match:
|
||||
return EntryFilterDecision(
|
||||
should_send=False,
|
||||
reason=f"Skipped because {blacklist_match.description}.",
|
||||
blacklist_match=blacklist_match,
|
||||
whitelist_match=whitelist_match,
|
||||
has_blacklist_filters=has_blacklist_filters,
|
||||
has_whitelist_filters=has_whitelist_filters,
|
||||
)
|
||||
|
||||
return EntryFilterDecision(
|
||||
should_send=True,
|
||||
reason="Sent because no active filter blocked it.",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<p class="mb-2">
|
||||
Plain text matching is case-insensitive and partial, so <code>orld</code> matches <code>World of Warcraft</code>.
|
||||
</p>
|
||||
<p class="mb-2">Whitelist matches still win. If an entry matches both, the preview keeps it as sent.</p>
|
||||
<p class="mb-2">Blacklist matches win. If an entry matches both, the preview keeps it as skipped.</p>
|
||||
<p class="mb-0">Keep the left side for editing and the right side for checking what gets removed.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<p class="mb-2">
|
||||
Plain text matching is case-insensitive and partial, so <code>orld</code> matches <code>World of Warcraft</code>.
|
||||
</p>
|
||||
<p class="mb-2">When an entry matches both lists, whitelist still wins and the preview shows it as sent.</p>
|
||||
<p class="mb-2">When an entry matches both lists, blacklist wins and the preview shows it as skipped.</p>
|
||||
<p class="mb-0">Saved blacklist rules remain active while you preview whitelist edits.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -207,8 +207,8 @@ def test_regex_should_be_skipped() -> None:
|
|||
assert entry_should_be_skipped(reader, first_entry[0]) is False, f"Entry should not be skipped: {first_entry[0]}"
|
||||
|
||||
|
||||
def test_whitelist_match_overrides_blacklist_match() -> None:
|
||||
"""A whitelist hit should beat a blacklist hit in the final decision."""
|
||||
def test_blacklist_match_overrides_whitelist_match() -> None:
|
||||
"""A blacklist hit should beat a whitelist hit in the final decision."""
|
||||
reader: Reader = get_reader()
|
||||
|
||||
reader.add_feed(feed_url)
|
||||
|
|
@ -232,10 +232,10 @@ def test_whitelist_match_overrides_blacklist_match() -> None:
|
|||
whitelist_values=get_filter_values_from_reader(reader, feed, "whitelist"),
|
||||
)
|
||||
|
||||
assert decision.should_send is True, "Whitelist match should override blacklist match"
|
||||
assert decision.should_send is False, "Blacklist match should override whitelist match"
|
||||
assert decision.blacklist_match is not None, "Expected a blacklist match"
|
||||
assert decision.whitelist_match is not None, "Expected a whitelist match"
|
||||
assert "whitelist overrides blacklist" in decision.reason
|
||||
assert "blacklist overrides whitelist" in decision.reason
|
||||
|
||||
|
||||
def test_blacklist_substring_match_on_title() -> None:
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ def test_blacklist_preview_does_not_persist_unsaved_rules() -> None:
|
|||
reader.delete_tag(feed_url, "blacklist_title")
|
||||
|
||||
|
||||
def test_whitelist_preview_shows_precedence_over_blacklist() -> None:
|
||||
def test_whitelist_preview_shows_blacklist_precedence() -> None:
|
||||
reader: Reader = ensure_preview_feed_exists()
|
||||
reader.set_tag(feed_url, "blacklist_title", "fvnnnfnfdnfdnfd") # pyright: ignore[reportArgumentType]
|
||||
|
||||
|
|
@ -297,8 +297,8 @@ def test_whitelist_preview_shows_precedence_over_blacklist() -> None:
|
|||
)
|
||||
|
||||
assert response.status_code == 200, f"/whitelist_preview failed: {response.text}"
|
||||
assert "whitelist overrides blacklist" in response.text
|
||||
assert "Sent" in response.text
|
||||
assert "blacklist overrides whitelist" in response.text
|
||||
assert "Skipped" in response.text
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
reader.delete_tag(feed_url, "blacklist_title")
|
||||
|
|
|
|||
|
|
@ -188,8 +188,8 @@ def test_regex_should_be_sent() -> None:
|
|||
assert should_be_sent(reader, first_entry[0]) is False, "Entry should not be sent"
|
||||
|
||||
|
||||
def test_active_whitelist_blocks_non_matching_blacklisted_entry() -> None:
|
||||
"""An active whitelist should block non-matching entries even if blacklist also matches."""
|
||||
def test_blacklist_blocks_when_active_whitelist_misses() -> None:
|
||||
"""A blacklist hit should block when an active whitelist does not match."""
|
||||
reader: Reader = get_reader()
|
||||
|
||||
reader.add_feed(feed_url)
|
||||
|
|
@ -213,10 +213,10 @@ def test_active_whitelist_blocks_non_matching_blacklisted_entry() -> None:
|
|||
whitelist_values=get_filter_values_from_reader(reader, feed, "whitelist"),
|
||||
)
|
||||
|
||||
assert decision.should_send is False, "Entry should be skipped when whitelist is active but does not match"
|
||||
assert decision.should_send is False, "Entry should be skipped when blacklist matches"
|
||||
assert decision.blacklist_match is not None, "Expected a blacklist match"
|
||||
assert decision.whitelist_match is None, "Expected whitelist to miss"
|
||||
assert "no whitelist rule matched" in decision.reason
|
||||
assert "blacklist text match on title" in decision.reason
|
||||
|
||||
|
||||
def test_whitelist_substring_match_on_title() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue