浏览代码

Add global block and keep filters

privatmamtora 1 年之前
父节点
当前提交
1a81866bb9

+ 4 - 0
client/model.go

@@ -42,6 +42,8 @@ type User struct {
 	CategoriesSortingOrder string     `json:"categories_sorting_order"`
 	MarkReadOnView         bool       `json:"mark_read_on_view"`
 	MediaPlaybackRate      float64    `json:"media_playback_rate"`
+	BlockFilterEntryRules  string     `json:"block_filter_entry_rules"`
+	KeepFilterEntryRules   string     `json:"keep_filter_entry_rules"`
 }
 
 func (u User) String() string {
@@ -82,6 +84,8 @@ type UserModificationRequest struct {
 	CategoriesSortingOrder *string  `json:"categories_sorting_order"`
 	MarkReadOnView         *bool    `json:"mark_read_on_view"`
 	MediaPlaybackRate      *float64 `json:"media_playback_rate"`
+	BlockFilterEntryRules  *string  `json:"block_filter_entry_rules"`
+	KeepFilterEntryRules   *string  `json:"keep_filter_entry_rules"`
 }
 
 // Users represents a list of users.

+ 9 - 0
internal/api/user.go

@@ -7,6 +7,7 @@ import (
 	json_parser "encoding/json"
 	"errors"
 	"net/http"
+	"regexp"
 
 	"miniflux.app/v2/internal/http/request"
 	"miniflux.app/v2/internal/http/response/json"
@@ -82,6 +83,14 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	cleanEnd := regexp.MustCompile(`(?m)\r\n\s*$`)
+	if userModificationRequest.BlockFilterEntryRules != nil {
+		*userModificationRequest.BlockFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.BlockFilterEntryRules, "")
+	}
+	if userModificationRequest.KeepFilterEntryRules != nil {
+		*userModificationRequest.KeepFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.KeepFilterEntryRules, "")
+	}
+
 	if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
 		json.BadRequest(w, r, validationErr.Error())
 		return

+ 9 - 0
internal/database/migrations.go

@@ -903,4 +903,13 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `
+			ALTER TABLE users
+				ADD COLUMN block_filter_entry_rules text not null default '',
+				ADD COLUMN keep_filter_entry_rules text not null default ''
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 9 - 0
internal/locale/translations/de_DE.json

@@ -302,6 +302,14 @@
     "error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
     "error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
     "error.settings_reading_speed_is_positive": "Die Lesegeschwindigkeiten müssen positive ganze Zahlen sein.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
     "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
     "error.feed_already_exists": "Dieser Feed existiert bereits.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
     "form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
     "form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML Datei",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Fever API aktivieren",

+ 9 - 0
internal/locale/translations/el_EL.json

@@ -302,6 +302,14 @@
     "error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
     "error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
     "error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
     "error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
     "error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Αρχείο OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Ενεργοποιήστε το Fever API",

+ 9 - 0
internal/locale/translations/en_US.json

@@ -302,6 +302,14 @@
     "error.password_min_length": "The password must have at least 6 characters.",
     "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
     "error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "The number of entries per page is not valid.",
     "error.feed_mandatory_fields": "The URL and the category are mandatory.",
     "error.feed_already_exists": "This feed already exists.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML file",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activate Fever API",

+ 9 - 0
internal/locale/translations/es_ES.json

@@ -295,6 +295,14 @@
     "error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
     "error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
     "error.settings_reading_speed_is_positive": "Las velocidades de lectura deben ser números enteros positivos.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "El número de artículos por página no es válido.",
     "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
     "error.feed_already_exists": "Este feed ya existe.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Archivo OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activar API de Fever",

+ 9 - 0
internal/locale/translations/fi_FI.json

@@ -302,6 +302,14 @@
     "error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
     "error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
     "error.settings_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
     "error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
     "error.feed_already_exists": "Tämä syöte on jo olemassa.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML-tiedosto",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Ota Fever API käyttöön",

+ 9 - 0
internal/locale/translations/fr_FR.json

@@ -295,6 +295,14 @@
     "error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
     "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
     "error.settings_reading_speed_is_positive": "Les vitesses de lecture doivent être des entiers positifs.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
     "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
     "error.feed_already_exists": "Ce flux existe déjà.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Paramètres de l'application",
     "form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
     "form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Fichier OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activer l'API de Fever",

+ 9 - 0
internal/locale/translations/hi_IN.json

@@ -302,6 +302,14 @@
     "error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
     "error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
     "error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
     "error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
     "error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "ओपीएमएल फ़ाइल",
     "form.import.label.url": "यूआरएल",
     "form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",

+ 9 - 0
internal/locale/translations/id_ID.json

@@ -292,6 +292,14 @@
     "error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
     "error.settings_mandatory_fields": "Harus ada nama pengguna, tema, bahasa, dan zona waktu.",
     "error.settings_reading_speed_is_positive": "Kecepatan membaca harus integer positif.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Jumlah entri per halaman tidak valid.",
     "error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
     "error.feed_already_exists": "Umpan ini sudah ada.",
@@ -372,6 +380,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Berkas OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Aktifkan API Fever",

+ 9 - 0
internal/locale/translations/it_IT.json

@@ -295,6 +295,14 @@
     "error.password_min_length": "La password deve contenere almeno 6 caratteri.",
     "error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
     "error.settings_reading_speed_is_positive": "Le velocità di lettura devono essere numeri interi positivi.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
     "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
     "error.feed_already_exists": "Questo feed esiste già.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "File OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Abilita l'API di Fever",

+ 9 - 0
internal/locale/translations/ja_JP.json

@@ -292,6 +292,14 @@
     "error.password_min_length": "パスワードは6文字以上である必要があります。",
     "error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのすべてが必要です。",
     "error.settings_reading_speed_is_positive": "読書速度は正の整数である必要があります。",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "ページあたりの記事数が無効です。",
     "error.feed_mandatory_fields": "URL と カテゴリが必要です。",
     "error.feed_already_exists": "このフィードは既に存在します。",
@@ -372,6 +380,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML ファイル",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Fever API を有効にする",

+ 9 - 0
internal/locale/translations/nl_NL.json

@@ -295,6 +295,14 @@
     "error.password_min_length": "Je moet minstens 6 tekens gebruiken.",
     "error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
     "error.settings_reading_speed_is_positive": "De leessnelheden moeten positieve gehele getallen zijn.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
     "error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
     "error.feed_already_exists": "Deze feed bestaat al.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML-bestand",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activeer Fever API",

+ 9 - 0
internal/locale/translations/pl_PL.json

@@ -305,6 +305,14 @@
     "error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
     "error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
     "error.settings_reading_speed_is_positive": "Prędkości odczytu muszą być dodatnimi liczbami całkowitymi.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
     "error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
     "error.feed_already_exists": "Ten kanał już istnieje.",
@@ -392,6 +400,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Plik OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Aktywuj Fever API",

+ 9 - 0
internal/locale/translations/pt_BR.json

@@ -295,6 +295,14 @@
     "error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
     "error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
     "error.settings_reading_speed_is_positive": "As velocidades de leitura devem ser inteiros positivos.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "O número de itens por página é inválido.",
     "error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
     "error.feed_already_exists": "Este feed já existe.",
@@ -382,6 +390,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Arquivo OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Ativar API do Fever",

+ 9 - 0
internal/locale/translations/ru_RU.json

@@ -305,6 +305,14 @@
     "error.password_min_length": "Вы должны использовать минимум 6 символов.",
     "error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
     "error.settings_reading_speed_is_positive": "Скорость чтения должна быть целым положительным числом.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Недопустимое значение количества записей на странице.",
     "error.feed_mandatory_fields": "Ссылка и категория обязательны.",
     "error.feed_already_exists": "Эта подписка уже существует.",
@@ -392,6 +400,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML файл",
     "form.import.label.url": "Ссылка",
     "form.integration.fever_activate": "Активировать Fever API",

+ 9 - 0
internal/locale/translations/tr_TR.json

@@ -122,6 +122,14 @@
   "error.settings_mandatory_fields": "Kullanıcı ad, tema, dil ve saat dilimi zorunlu.",
   "error.settings_media_playback_rate_range": "Oynatma hızı aralık dışında",
   "error.settings_reading_speed_is_positive": "Okuma hızları pozitif tam sayılar olmalıdır.",
+  "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+  "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+  "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+  "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+  "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+  "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+  "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+  "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
   "error.site_url_not_empty": "Site URL'si boş olamaz.",
   "error.subscription_not_found": "Herhangi bir abonelik bulunamadı.",
   "error.title_required": "Başlık zorunlu.",
@@ -264,6 +272,7 @@
   "form.prefs.fieldset.application_settings": "Uygulama Ayarları",
   "form.prefs.fieldset.authentication_settings": "Kimlik Doğrulama Ayarları",
   "form.prefs.fieldset.reader_settings": "Okuyucu Ayarları",
+  "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
   "form.prefs.label.categories_sorting_order": "Kategori sıralaması",
   "form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)",
   "form.prefs.label.custom_css": "Özel CSS",

+ 9 - 0
internal/locale/translations/uk_UA.json

@@ -312,6 +312,14 @@
     "error.password_min_length": "Пароль має складати щонайменше 6 символів.",
     "error.settings_mandatory_fields": "Поля імені, теми, мови та часового поясу є обов’язковими.",
     "error.settings_reading_speed_is_positive": "Швидкість читання має бути додатнім цілим числом.",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "Число записів на сторінку недійсне.",
     "error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
     "error.feed_already_exists": "Така стрічка вже існує.",
@@ -392,6 +400,7 @@
     "form.prefs.fieldset.application_settings": "Application Settings",
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Файл OPML",
     "form.import.label.url": "URL-адреса",
     "form.integration.fever_activate": "Увімкнути API Fever",

+ 9 - 0
internal/locale/translations/zh_CN.json

@@ -293,6 +293,14 @@
     "error.site_url_not_empty": "源网站的网址不能为空。",
     "error.feed_title_not_empty": "订阅源的标题不能为空。",
     "error.settings_reading_speed_is_positive": "阅读速度必须是正整数。",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.feed_category_not_found": "此类别不存在或不属于该用户。",
     "error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
     "error.feed_invalid_keeplist_rule": "保留列表规则无效。",
@@ -372,6 +380,7 @@
     "form.prefs.fieldset.application_settings": "应用设置",
     "form.prefs.fieldset.authentication_settings": "用户认证设置",
     "form.prefs.fieldset.reader_settings": "阅读器设置",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML 文件",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "启用 Fever API",

+ 9 - 0
internal/locale/translations/zh_TW.json

@@ -285,6 +285,14 @@
     "error.password_min_length": "請至少輸入 6 個字元",
     "error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區",
     "error.settings_reading_speed_is_positive": "閱讀速度必須是正整數。",
+    "error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
+    "error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
+    "error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
+    "error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
+    "error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
+    "error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
     "error.entries_per_page_invalid": "每頁的文章數無效。",
     "error.feed_mandatory_fields": "必須填寫網址和分類",
     "error.feed_already_exists": "此Feed已存在。",
@@ -372,6 +380,7 @@
     "form.prefs.fieldset.application_settings": "應用程式設定",
     "form.prefs.fieldset.authentication_settings": "使用者認證設定",
     "form.prefs.fieldset.reader_settings": "閱讀器設定",
+    "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML 檔案",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "啟用 Fever API",

+ 12 - 0
internal/model/user.go

@@ -36,6 +36,8 @@ type User struct {
 	CategoriesSortingOrder string     `json:"categories_sorting_order"`
 	MarkReadOnView         bool       `json:"mark_read_on_view"`
 	MediaPlaybackRate      float64    `json:"media_playback_rate"`
+	BlockFilterEntryRules  string     `json:"block_filter_entry_rules"`
+	KeepFilterEntryRules   string     `json:"keep_filter_entry_rules"`
 }
 
 // UserCreationRequest represents the request to create a user.
@@ -72,6 +74,8 @@ type UserModificationRequest struct {
 	CategoriesSortingOrder *string  `json:"categories_sorting_order"`
 	MarkReadOnView         *bool    `json:"mark_read_on_view"`
 	MediaPlaybackRate      *float64 `json:"media_playback_rate"`
+	BlockFilterEntryRules  *string  `json:"block_filter_entry_rules"`
+	KeepFilterEntryRules   *string  `json:"keep_filter_entry_rules"`
 }
 
 // Patch updates the User object with the modification request.
@@ -167,6 +171,14 @@ func (u *UserModificationRequest) Patch(user *User) {
 	if u.MediaPlaybackRate != nil {
 		user.MediaPlaybackRate = *u.MediaPlaybackRate
 	}
+
+	if u.BlockFilterEntryRules != nil {
+		user.BlockFilterEntryRules = *u.BlockFilterEntryRules
+	}
+
+	if u.KeepFilterEntryRules != nil {
+		user.KeepFilterEntryRules = *u.KeepFilterEntryRules
+	}
 }
 
 // UseTimezone converts last login date to the given timezone.

+ 83 - 3
internal/reader/processor/processor.go

@@ -10,6 +10,7 @@ import (
 	"regexp"
 	"slices"
 	"strconv"
+	"strings"
 	"time"
 
 	"miniflux.app/v2/internal/config"
@@ -51,7 +52,7 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
 			slog.Int64("feed_id", feed.ID),
 			slog.String("feed_url", feed.FeedURL),
 		)
-		if isBlockedEntry(feed, entry) || !isAllowedEntry(feed, entry) || !isRecentEntry(entry) {
+		if isBlockedEntry(feed, entry, user) || !isAllowedEntry(feed, entry, user) || !isRecentEntry(entry) {
 			continue
 		}
 
@@ -121,7 +122,46 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
 	feed.Entries = filteredEntries
 }
 
-func isBlockedEntry(feed *model.Feed, entry *model.Entry) bool {
+func isBlockedEntry(feed *model.Feed, entry *model.Entry, user *model.User) bool {
+	if user.BlockFilterEntryRules != "" {
+		rules := strings.Split(user.BlockFilterEntryRules, "\n")
+		for _, rule := range rules {
+			parts := strings.SplitN(rule, "=", 2)
+
+			var match bool
+			switch parts[0] {
+			case "EntryTitle":
+				match, _ = regexp.MatchString(parts[1], entry.Title)
+			case "EntryURL":
+				match, _ = regexp.MatchString(parts[1], entry.URL)
+			case "EntryCommentsURL":
+				match, _ = regexp.MatchString(parts[1], entry.CommentsURL)
+			case "EntryContent":
+				match, _ = regexp.MatchString(parts[1], entry.Content)
+			case "EntryAuthor":
+				match, _ = regexp.MatchString(parts[1], entry.Author)
+			case "EntryTag":
+				containsTag := slices.ContainsFunc(entry.Tags, func(tag string) bool {
+					match, _ = regexp.MatchString(parts[1], tag)
+					return match
+				})
+				if containsTag {
+					match = true
+				}
+			}
+
+			if match {
+				slog.Debug("Blocking entry based on rule",
+					slog.String("entry_url", entry.URL),
+					slog.Int64("feed_id", feed.ID),
+					slog.String("feed_url", feed.FeedURL),
+					slog.String("rule", rule),
+				)
+				return true
+			}
+		}
+	}
+
 	if feed.BlocklistRules == "" {
 		return false
 	}
@@ -152,7 +192,47 @@ func isBlockedEntry(feed *model.Feed, entry *model.Entry) bool {
 	return false
 }
 
-func isAllowedEntry(feed *model.Feed, entry *model.Entry) bool {
+func isAllowedEntry(feed *model.Feed, entry *model.Entry, user *model.User) bool {
+	if user.KeepFilterEntryRules != "" {
+		rules := strings.Split(user.KeepFilterEntryRules, "\n")
+		for _, rule := range rules {
+			parts := strings.SplitN(rule, "=", 2)
+
+			var match bool
+			switch parts[0] {
+			case "EntryTitle":
+				match, _ = regexp.MatchString(parts[1], entry.Title)
+			case "EntryURL":
+				match, _ = regexp.MatchString(parts[1], entry.URL)
+			case "EntryCommentsURL":
+				match, _ = regexp.MatchString(parts[1], entry.CommentsURL)
+			case "EntryContent":
+				match, _ = regexp.MatchString(parts[1], entry.Content)
+			case "EntryAuthor":
+				match, _ = regexp.MatchString(parts[1], entry.Author)
+			case "EntryTag":
+				containsTag := slices.ContainsFunc(entry.Tags, func(tag string) bool {
+					match, _ = regexp.MatchString(parts[1], tag)
+					return match
+				})
+				if containsTag {
+					match = true
+				}
+			}
+
+			if match {
+				slog.Debug("Allowing entry based on rule",
+					slog.String("entry_url", entry.URL),
+					slog.Int64("feed_id", feed.ID),
+					slog.String("feed_url", feed.FeedURL),
+					slog.String("rule", rule),
+				)
+				return true
+			}
+		}
+		return false
+	}
+
 	if feed.KeeplistRules == "" {
 		return true
 	}

+ 44 - 24
internal/reader/processor/processor_test.go

@@ -15,23 +15,33 @@ func TestBlockingEntries(t *testing.T) {
 	var scenarios = []struct {
 		feed     *model.Feed
 		entry    *model.Entry
+		user     *model.User
 		expected bool
 	}{
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{URL: "https://example.com"}, true},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{URL: "https://different.com"}, false},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Some Example"}, true},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different"}, false},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Tags: []string{"example", "something else"}}, true},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"example", "something else"}}, true},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"something different", "something else"}}, true},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Tags: []string{"something different", "something else"}}, false},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Example"}, true},
-		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Something different"}, false},
-		{&model.Feed{ID: 1}, &model.Entry{Title: "No rule defined"}, false},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{URL: "https://example.com"}, &model.User{}, true},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{URL: "https://different.com"}, &model.User{}, false},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Some Example"}, &model.User{}, true},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different"}, &model.User{}, false},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Tags: []string{"example", "something else"}}, &model.User{}, true},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"example", "something else"}}, &model.User{}, true},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"something different", "something else"}}, &model.User{}, true},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Tags: []string{"something different", "something else"}}, &model.User{}, false},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Example"}, &model.User{}, true},
+		{&model.Feed{ID: 1, BlocklistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Something different"}, &model.User{}, false},
+		{&model.Feed{ID: 1}, &model.Entry{Title: "No rule defined"}, &model.User{}, false},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{URL: "https://example.com", Title: "Some Example"}, &model.User{BlockFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{URL: "https://different.com", Title: "Some Test"}, &model.User{BlockFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{URL: "https://different.com", Title: "Some Example"}, &model.User{BlockFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, false},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{CommentsURL: "https://example.com", Content: "Some Example"}, &model.User{BlockFilterEntryRules: "EntryCommentsURL=(?i)example\nEntryContent=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{CommentsURL: "https://different.com", Content: "Some Test"}, &model.User{BlockFilterEntryRules: "EntryCommentsURL=(?i)example\nEntryContent=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{CommentsURL: "https://different.com", Content: "Some Example"}, &model.User{BlockFilterEntryRules: "EntryCommentsURL=(?i)example\nEntryContent=(?i)Test"}, false},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{Author: "Example", Tags: []string{"example", "something else"}}, &model.User{BlockFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{Author: "Different", Tags: []string{"example", "something else"}}, &model.User{BlockFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)example"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{Author: "Different", Tags: []string{"example", "something else"}}, &model.User{BlockFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)Test"}, false},
 	}
 
 	for _, tc := range scenarios {
-		result := isBlockedEntry(tc.feed, tc.entry)
+		result := isBlockedEntry(tc.feed, tc.entry, tc.user)
 		if tc.expected != result {
 			t.Errorf(`Unexpected result, got %v for entry %q`, result, tc.entry.Title)
 		}
@@ -42,23 +52,33 @@ func TestAllowEntries(t *testing.T) {
 	var scenarios = []struct {
 		feed     *model.Feed
 		entry    *model.Entry
+		user     *model.User
 		expected bool
 	}{
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "https://example.com"}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "https://different.com"}, false},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Some Example"}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different"}, false},
-		{&model.Feed{ID: 1}, &model.Entry{Title: "No rule defined"}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different", Tags: []string{"example", "something else"}}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"example", "something else"}}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"something different", "something else"}}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something more", Tags: []string{"something different", "something else"}}, false},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Example"}, true},
-		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Something different"}, false},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "https://example.com"}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "https://different.com"}, &model.User{}, false},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Some Example"}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different"}, &model.User{}, false},
+		{&model.Feed{ID: 1}, &model.Entry{Title: "No rule defined"}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different", Tags: []string{"example", "something else"}}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"example", "something else"}}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Example", Tags: []string{"something different", "something else"}}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something more", Tags: []string{"something different", "something else"}}, &model.User{}, false},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Example"}, &model.User{}, true},
+		{&model.Feed{ID: 1, KeeplistRules: "(?i)example"}, &model.Entry{Title: "Something different", Author: "Something different"}, &model.User{}, false},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{URL: "https://example.com", Title: "Some Example"}, &model.User{KeepFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{URL: "https://different.com", Title: "Some Test"}, &model.User{KeepFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{URL: "https://different.com", Title: "Some Example"}, &model.User{KeepFilterEntryRules: "EntryURL=(?i)example\nEntryTitle=(?i)Test"}, false},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{CommentsURL: "https://example.com", Content: "Some Example"}, &model.User{KeepFilterEntryRules: "EntryCommentsURL=(?i)example\nEntryContent=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{CommentsURL: "https://different.com", Content: "Some Test"}, &model.User{KeepFilterEntryRules: "EntryCommentsURL=(?i)example\nEntryContent=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{CommentsURL: "https://different.com", Content: "Some Example"}, &model.User{KeepFilterEntryRules: "EntryCommentsURL=(?i)example\nEntryContent=(?i)Test"}, false},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{Author: "Example", Tags: []string{"example", "something else"}}, &model.User{KeepFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)Test"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{Author: "Different", Tags: []string{"example", "something else"}}, &model.User{KeepFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)example"}, true},
+		{&model.Feed{ID: 1, BlocklistRules: ""}, &model.Entry{Author: "Different", Tags: []string{"example", "something else"}}, &model.User{KeepFilterEntryRules: "EntryAuthor=(?i)example\nEntryTag=(?i)Test"}, false},
 	}
 
 	for _, tc := range scenarios {
-		result := isAllowedEntry(tc.feed, tc.entry)
+		result := isAllowedEntry(tc.feed, tc.entry, tc.user)
 		if tc.expected != result {
 			t.Errorf(`Unexpected result, got %v for entry %q`, result, tc.entry.Title)
 		}

+ 36 - 10
internal/storage/user.go

@@ -92,7 +92,9 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
-			media_playback_rate
+			media_playback_rate,
+			block_filter_entry_rules,
+			keep_filter_entry_rules
 	`
 
 	tx, err := s.db.Begin()
@@ -132,6 +134,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 		&user.CategoriesSortingOrder,
 		&user.MarkReadOnView,
 		&user.MediaPlaybackRate,
+		&user.BlockFilterEntryRules,
+		&user.KeepFilterEntryRules,
 	)
 	if err != nil {
 		tx.Rollback()
@@ -189,9 +193,11 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				default_home_page=$20,
 				categories_sorting_order=$21,
 				mark_read_on_view=$22,
-				media_playback_rate=$23
+				media_playback_rate=$23,
+				block_filter_entry_rules=$24,
+				keep_filter_entry_rules=$25
 			WHERE
-				id=$24
+				id=$26
 		`
 
 		_, err = s.db.Exec(
@@ -219,6 +225,8 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.CategoriesSortingOrder,
 			user.MarkReadOnView,
 			user.MediaPlaybackRate,
+			user.BlockFilterEntryRules,
+			user.KeepFilterEntryRules,
 			user.ID,
 		)
 		if err != nil {
@@ -248,9 +256,11 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				default_home_page=$19,
 				categories_sorting_order=$20,
 				mark_read_on_view=$21,
-				media_playback_rate=$22
+				media_playback_rate=$22,
+				block_filter_entry_rules=$23,
+				keep_filter_entry_rules=$24
 			WHERE
-				id=$23
+				id=$25
 		`
 
 		_, err := s.db.Exec(
@@ -277,6 +287,8 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.CategoriesSortingOrder,
 			user.MarkReadOnView,
 			user.MediaPlaybackRate,
+			user.BlockFilterEntryRules,
+			user.KeepFilterEntryRules,
 			user.ID,
 		)
 
@@ -325,7 +337,9 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
-			media_playback_rate
+			media_playback_rate,
+			block_filter_entry_rules,
+			keep_filter_entry_rules
 		FROM
 			users
 		WHERE
@@ -361,7 +375,9 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
-			media_playback_rate
+			media_playback_rate,
+			block_filter_entry_rules,
+			keep_filter_entry_rules
 		FROM
 			users
 		WHERE
@@ -397,7 +413,9 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
-			media_playback_rate
+			media_playback_rate,
+			block_filter_entry_rules,
+			keep_filter_entry_rules
 		FROM
 			users
 		WHERE
@@ -440,7 +458,9 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
 			u.default_home_page,
 			u.categories_sorting_order,
 			u.mark_read_on_view,
-			media_playback_rate
+			media_playback_rate,
+			u.block_filter_entry_rules,
+			u.keep_filter_entry_rules
 		FROM
 			users u
 		LEFT JOIN
@@ -478,6 +498,8 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
 		&user.CategoriesSortingOrder,
 		&user.MarkReadOnView,
 		&user.MediaPlaybackRate,
+		&user.BlockFilterEntryRules,
+		&user.KeepFilterEntryRules,
 	)
 
 	if err == sql.ErrNoRows {
@@ -586,7 +608,9 @@ func (s *Storage) Users() (model.Users, error) {
 			default_home_page,
 			categories_sorting_order,
 			mark_read_on_view,
-			media_playback_rate
+			media_playback_rate,
+			block_filter_entry_rules,
+			keep_filter_entry_rules
 		FROM
 			users
 		ORDER BY username ASC
@@ -625,6 +649,8 @@ func (s *Storage) Users() (model.Users, error) {
 			&user.CategoriesSortingOrder,
 			&user.MarkReadOnView,
 			&user.MediaPlaybackRate,
+			&user.BlockFilterEntryRules,
+			&user.KeepFilterEntryRules,
 		)
 
 		if err != nil {

+ 22 - 0
internal/template/templates/views/settings.html

@@ -200,6 +200,28 @@
 
         <label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label>
         <textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
+
+        <div class="buttons">
+            <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
+        </div>
+    </fieldset>
+
+    <fieldset>
+        <legend>{{ t "form.prefs.fieldset.global_feed_settings" }}</legend>
+        <div class="form-label-row">
+            <label for="form-blocklist-rules">
+                {{ t "form.feed.label.blocklist_rules" }}
+            </label>
+        </div>
+        <textarea id="form-blocklist-rules" name="block_filter_entry_rules" cols="40" rows="10" spellcheck="false">{{ .form.BlockFilterEntryRules }}</textarea>
+        
+        <div class="form-label-row">
+            <label for="form-keeplist-rules">
+                {{ t "form.feed.label.keeplist_rules" }}
+            </label>
+        </div>
+        <textarea id="form-keeplist-rules" name="keep_filter_entry_rules" cols="40" rows="10" spellcheck="false">{{ .form.KeepFilterEntryRules }}</textarea>        
+
         <div class="buttons">
             <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
         </div>

+ 6 - 0
internal/ui/form/settings.go

@@ -34,6 +34,8 @@ type SettingsForm struct {
 	CategoriesSortingOrder string
 	MarkReadOnView         bool
 	MediaPlaybackRate      float64
+	BlockFilterEntryRules  string
+	KeepFilterEntryRules   string
 }
 
 // Merge updates the fields of the given user.
@@ -57,6 +59,8 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 	user.CategoriesSortingOrder = s.CategoriesSortingOrder
 	user.MarkReadOnView = s.MarkReadOnView
 	user.MediaPlaybackRate = s.MediaPlaybackRate
+	user.BlockFilterEntryRules = s.BlockFilterEntryRules
+	user.KeepFilterEntryRules = s.KeepFilterEntryRules
 
 	if s.Password != "" {
 		user.Password = s.Password
@@ -133,5 +137,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
 		CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
 		MarkReadOnView:         r.FormValue("mark_read_on_view") == "1",
 		MediaPlaybackRate:      mediaPlaybackRate,
+		BlockFilterEntryRules:  r.FormValue("block_filter_entry_rules"),
+		KeepFilterEntryRules:   r.FormValue("keep_filter_entry_rules"),
 	}
 }

+ 2 - 0
internal/ui/settings_show.go

@@ -42,6 +42,8 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 		CategoriesSortingOrder: user.CategoriesSortingOrder,
 		MarkReadOnView:         user.MarkReadOnView,
 		MediaPlaybackRate:      user.MediaPlaybackRate,
+		BlockFilterEntryRules:  user.BlockFilterEntryRules,
+		KeepFilterEntryRules:   user.KeepFilterEntryRules,
 	}
 
 	timezones, err := h.store.Timezones()

+ 21 - 13
internal/ui/settings_update.go

@@ -5,6 +5,7 @@ package ui // import "miniflux.app/v2/internal/ui"
 
 import (
 	"net/http"
+	"regexp"
 
 	"miniflux.app/v2/internal/http/request"
 	"miniflux.app/v2/internal/http/response/html"
@@ -53,6 +54,11 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
 	view.Set("countWebAuthnCerts", h.store.CountWebAuthnCredentialsByUserID(loggedUser.ID))
 	view.Set("webAuthnCerts", creds)
 
+	// Sanitize the end of the block & Keep rules
+	cleanEnd := regexp.MustCompile(`(?m)\r\n\s*$`)
+	settingsForm.BlockFilterEntryRules = cleanEnd.ReplaceAllLiteralString(settingsForm.BlockFilterEntryRules, "")
+	settingsForm.KeepFilterEntryRules = cleanEnd.ReplaceAllLiteralString(settingsForm.KeepFilterEntryRules, "")
+
 	if validationErr := settingsForm.Validate(); validationErr != nil {
 		view.Set("errorMessage", validationErr.Translate(loggedUser.Language))
 		html.OK(w, r, view.Render("settings"))
@@ -60,19 +66,21 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
 	}
 
 	userModificationRequest := &model.UserModificationRequest{
-		Username:            model.OptionalString(settingsForm.Username),
-		Password:            model.OptionalString(settingsForm.Password),
-		Theme:               model.OptionalString(settingsForm.Theme),
-		Language:            model.OptionalString(settingsForm.Language),
-		Timezone:            model.OptionalString(settingsForm.Timezone),
-		EntryDirection:      model.OptionalString(settingsForm.EntryDirection),
-		EntriesPerPage:      model.OptionalNumber(settingsForm.EntriesPerPage),
-		DisplayMode:         model.OptionalString(settingsForm.DisplayMode),
-		GestureNav:          model.OptionalString(settingsForm.GestureNav),
-		DefaultReadingSpeed: model.OptionalNumber(settingsForm.DefaultReadingSpeed),
-		CJKReadingSpeed:     model.OptionalNumber(settingsForm.CJKReadingSpeed),
-		DefaultHomePage:     model.OptionalString(settingsForm.DefaultHomePage),
-		MediaPlaybackRate:   model.OptionalNumber(settingsForm.MediaPlaybackRate),
+		Username:              model.OptionalString(settingsForm.Username),
+		Password:              model.OptionalString(settingsForm.Password),
+		Theme:                 model.OptionalString(settingsForm.Theme),
+		Language:              model.OptionalString(settingsForm.Language),
+		Timezone:              model.OptionalString(settingsForm.Timezone),
+		EntryDirection:        model.OptionalString(settingsForm.EntryDirection),
+		EntriesPerPage:        model.OptionalNumber(settingsForm.EntriesPerPage),
+		DisplayMode:           model.OptionalString(settingsForm.DisplayMode),
+		GestureNav:            model.OptionalString(settingsForm.GestureNav),
+		DefaultReadingSpeed:   model.OptionalNumber(settingsForm.DefaultReadingSpeed),
+		CJKReadingSpeed:       model.OptionalNumber(settingsForm.CJKReadingSpeed),
+		DefaultHomePage:       model.OptionalString(settingsForm.DefaultHomePage),
+		MediaPlaybackRate:     model.OptionalNumber(settingsForm.MediaPlaybackRate),
+		BlockFilterEntryRules: model.OptionalString(settingsForm.BlockFilterEntryRules),
+		KeepFilterEntryRules:  model.OptionalString(settingsForm.KeepFilterEntryRules),
 	}
 
 	if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil {

+ 47 - 0
internal/validator/user.go

@@ -4,6 +4,9 @@
 package validator // import "miniflux.app/v2/internal/validator"
 
 import (
+	"slices"
+	"strings"
+
 	"miniflux.app/v2/internal/locale"
 	"miniflux.app/v2/internal/model"
 	"miniflux.app/v2/internal/storage"
@@ -108,6 +111,18 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod
 		}
 	}
 
+	if changes.BlockFilterEntryRules != nil {
+		if err := isValidFilterRules(*changes.BlockFilterEntryRules, "block"); err != nil {
+			return err
+		}
+	}
+
+	if changes.KeepFilterEntryRules != nil {
+		if err := isValidFilterRules(*changes.KeepFilterEntryRules, "keep"); err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
 
@@ -195,3 +210,35 @@ func validateMediaPlaybackRate(mediaPlaybackRate float64) *locale.LocalizedError
 	}
 	return nil
 }
+
+func isValidFilterRules(filterEntryRules string, filterType string) *locale.LocalizedError {
+	// Valid Format: FieldName(RegEx)~FieldName(RegEx)~...
+	fieldNames := []string{"EntryTitle", "EntryURL", "EntryCommentsURL", "EntryContent", "EntryAuthor", "EntryTag"}
+
+	rules := strings.Split(filterEntryRules, "\n")
+	for i, rule := range rules {
+		// Check if rule starts with a valid fieldName
+		idx := slices.IndexFunc(fieldNames, func(fieldName string) bool { return strings.HasPrefix(rule, fieldName) })
+		if idx == -1 {
+			return locale.NewLocalizedError("error.settings_"+filterType+"_rule_fieldname_invalid", i+1, "'"+strings.Join(fieldNames, "', '")+"'")
+		}
+		fieldName := fieldNames[idx]
+		fieldRegEx, _ := strings.CutPrefix(rule, fieldName)
+
+		// Check if regex begins with a =
+		if !strings.HasPrefix(fieldRegEx, "=") {
+			return locale.NewLocalizedError("error.settings_"+filterType+"_rule_separator_required", i+1)
+		}
+		fieldRegEx = strings.TrimPrefix(fieldRegEx, "=")
+
+		if fieldRegEx == "" {
+			return locale.NewLocalizedError("error.settings_"+filterType+"_rule_regex_required", i+1)
+		}
+
+		// Check if provided pattern is a valid RegEx
+		if !IsValidRegex(fieldRegEx) {
+			return locale.NewLocalizedError("error.settings_"+filterType+"_rule_invalid_regex", i+1)
+		}
+	}
+	return nil
+}