diff --git a/client/model.go b/client/model.go index c0d42eb8..a90762ad 100644 --- a/client/model.go +++ b/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. diff --git a/internal/api/user.go b/internal/api/user.go index 8c660a57..d507d550 100644 --- a/internal/api/user.go +++ b/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 diff --git a/internal/database/migrations.go b/internal/database/migrations.go index e48217ad..e38c657e 100644 --- a/internal/database/migrations.go +++ b/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 + }, } diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index ff297ba6..607dda09 100644 --- a/internal/locale/translations/de_DE.json +++ b/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", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 932c7c47..53e04d16 100644 --- a/internal/locale/translations/el_EL.json +++ b/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", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 872f63f5..0bfcc4bb 100644 --- a/internal/locale/translations/en_US.json +++ b/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", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index b52d47d1..570b6885 100644 --- a/internal/locale/translations/es_ES.json +++ b/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", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index 00e4c8f4..ebd620a2 100644 --- a/internal/locale/translations/fi_FI.json +++ b/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", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 63d7d790..6a2b3993 100644 --- a/internal/locale/translations/fr_FR.json +++ b/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", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index 6d956521..1e806c83 100644 --- a/internal/locale/translations/hi_IN.json +++ b/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": "फीवर एपीआई सक्रिय करें", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 001687b3..9114af8d 100644 --- a/internal/locale/translations/id_ID.json +++ b/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", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index f62526f0..630f688b 100644 --- a/internal/locale/translations/it_IT.json +++ b/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", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index c0c68923..f68b2242 100644 --- a/internal/locale/translations/ja_JP.json +++ b/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 を有効にする", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index 134ddcb9..4843a5c9 100644 --- a/internal/locale/translations/nl_NL.json +++ b/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", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index b8bf855d..b61fbced 100644 --- a/internal/locale/translations/pl_PL.json +++ b/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", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index d014c603..6a4486d5 100644 --- a/internal/locale/translations/pt_BR.json +++ b/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", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 47df1f61..6d91f121 100644 --- a/internal/locale/translations/ru_RU.json +++ b/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", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index 965f68de..53064e09 100644 --- a/internal/locale/translations/tr_TR.json +++ b/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", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index f8496ad6..b9c6c7f7 100644 --- a/internal/locale/translations/uk_UA.json +++ b/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", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 5b9387fa..0ddcc24a 100644 --- a/internal/locale/translations/zh_CN.json +++ b/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", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index 79020d9f..2c84bd7b 100644 --- a/internal/locale/translations/zh_TW.json +++ b/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", diff --git a/internal/model/user.go b/internal/model/user.go index 62aff600..3a1b7865 100644 --- a/internal/model/user.go +++ b/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. diff --git a/internal/reader/processor/processor.go b/internal/reader/processor/processor.go index deaf25f3..06a5cbc5 100644 --- a/internal/reader/processor/processor.go +++ b/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 } diff --git a/internal/reader/processor/processor_test.go b/internal/reader/processor/processor_test.go index 48ae2d6b..57d34e47 100644 --- a/internal/reader/processor/processor_test.go +++ b/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) } diff --git a/internal/storage/user.go b/internal/storage/user.go index 4f30ac0d..f783348f 100644 --- a/internal/storage/user.go +++ b/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 { diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html index 0be77f62..dcd8dd0f 100644 --- a/internal/template/templates/views/settings.html +++ b/internal/template/templates/views/settings.html @@ -200,6 +200,28 @@ + +
+ +
+ + +
+ {{ t "form.prefs.fieldset.global_feed_settings" }} +
+ +
+ + +
+ +
+ +
diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go index a46d9714..05b5b927 100644 --- a/internal/ui/form/settings.go +++ b/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"), } } diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go index 3a96b29c..e89cb473 100644 --- a/internal/ui/settings_show.go +++ b/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() diff --git a/internal/ui/settings_update.go b/internal/ui/settings_update.go index e84cd1ff..e0059d4d 100644 --- a/internal/ui/settings_update.go +++ b/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 { diff --git a/internal/validator/user.go b/internal/validator/user.go index a397167e..57b3119d 100644 --- a/internal/validator/user.go +++ b/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 +}