From c4832831bc6a8c3a91db8874159b62606e62b004 Mon Sep 17 00:00:00 2001 From: Sushant Bhadauria Date: Mon, 1 Jun 2026 22:35:53 +0530 Subject: [PATCH] api: add adlist filter to top blocked domains endpoint Add an optional `list` query parameter to GET /api/stats/top_domains that restricts results to domains belonging to a specific subscription list (adlist) identified by its integer ID. - api/stats.c: parse `list` param in api_stats_top_domains(); extend get_top_domains() with adlist_id (-1 = all lists, unchanged behaviour); skip non-matching domains via gravityDB_domain_in_adlist() - database/gravity-db.c/.h: add gravityDB_domain_in_adlist() which queries gravity WHERE domain = ?1 AND adlist_id = ?2 - api/padd.c, api/queries.c: update existing callers to pass -1 - api/docs/content/specs/stats.yaml: document the new list parameter Co-authored-by: Cursor --- src/api/api.h | 3 +- src/api/docs/content/specs/stats.yaml | 13 +++++++ src/api/padd.c | 4 +- src/api/queries.c | 4 +- src/api/stats.c | 22 +++++++++-- src/database/gravity-db.c | 56 +++++++++++++++++++++++++++ src/database/gravity-db.h | 2 + 7 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/api/api.h b/src/api/api.h index 4e39772c2a..2a3275c474 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -35,7 +35,8 @@ int api_stats_top_domains(struct ftl_conn *api); int api_stats_top_clients(struct ftl_conn *api); int api_stats_recentblocked(struct ftl_conn *api); cJSON *get_top_domains(struct ftl_conn *api, const int count, - const bool blocked, const bool domains_only); + const bool blocked, const bool domains_only, + const int adlist_id); cJSON *get_top_clients(struct ftl_conn *api, const int count, const bool blocked, const bool clients_only, const bool names_only, const bool ip_if_no_name); diff --git a/src/api/docs/content/specs/stats.yaml b/src/api/docs/content/specs/stats.yaml index 919ef7af8d..833a7455ed 100644 --- a/src/api/docs/content/specs/stats.yaml +++ b/src/api/docs/content/specs/stats.yaml @@ -148,6 +148,7 @@ components: parameters: - $ref: 'stats.yaml#/components/parameters/top_items/blocked' - $ref: 'stats.yaml#/components/parameters/top_items/count' + - $ref: 'stats.yaml#/components/parameters/top_items/list' responses: '200': description: OK @@ -799,3 +800,15 @@ components: type: integer required: false example: 10 + list: + in: query + description: > + Filter results to domains that belong to a specific subscription list + (adlist). The value is the integer ID of the adlist as returned by + GET /api/lists. When omitted (or negative), results from all lists + are returned. + name: list + schema: + type: integer + required: false + example: 1 diff --git a/src/api/padd.c b/src/api/padd.c index 9ef59e2d91..7fb61d5585 100644 --- a/src/api/padd.c +++ b/src/api/padd.c @@ -75,7 +75,7 @@ int api_padd(struct ftl_conn *api) JSON_ADD_NUMBER_TO_OBJECT(json, "active_clients", active_clients); JSON_ADD_NUMBER_TO_OBJECT(json, "gravity_size", num_gravity); - cJSON *top_domains = get_top_domains(api, 1, false, true); + cJSON *top_domains = get_top_domains(api, 1, false, true, -1); if(cJSON_GetArraySize(top_domains) == 0) { JSON_ADD_NULL_TO_OBJECT(json, "top_domain"); @@ -87,7 +87,7 @@ int api_padd(struct ftl_conn *api) JSON_COPY_STR_TO_OBJECT(json, "top_domain", domain); } cJSON_Delete(top_domains); - cJSON *top_blocked = get_top_domains(api, 1, true, true); + cJSON *top_blocked = get_top_domains(api, 1, true, true, -1); if(cJSON_GetArraySize(top_blocked) == 0) { JSON_ADD_NULL_TO_OBJECT(json, "top_blocked"); diff --git a/src/api/queries.c b/src/api/queries.c index ce533708af..409097bd2c 100644 --- a/src/api/queries.c +++ b/src/api/queries.c @@ -88,8 +88,8 @@ int api_queries_suggestions(struct ftl_conn *api) get_int_var(api->request->query_string, "count", &count); // Get domains - cJSON *domain = get_top_domains(api, count, false, true); - cJSON *blocked = get_top_domains(api, count, true, true); + cJSON *domain = get_top_domains(api, count, false, true, -1); + cJSON *blocked = get_top_domains(api, count, true, true, -1); // Add domains from both arrays, avoiding duplicates cJSON *entry = NULL; cJSON_ArrayForEach(entry, blocked) diff --git a/src/api/stats.c b/src/api/stats.c index 56280d7b71..5a2bd1ae04 100644 --- a/src/api/stats.c +++ b/src/api/stats.c @@ -22,6 +22,8 @@ #include "overTime.h" // enum REGEX #include "regex_r.h" +// gravityDB_domain_in_adlist() +#include "database/gravity-db.h" // sqrt() #include @@ -177,7 +179,8 @@ int api_stats_summary(struct ftl_conn *api) } cJSON *get_top_domains(struct ftl_conn *api, const int count, - const bool blocked, const bool domains_only) + const bool blocked, const bool domains_only, + const int adlist_id) { // Exit before processing any data if requested via config setting if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS) @@ -278,6 +281,14 @@ cJSON *get_top_domains(struct ftl_conn *api, const int count, } } + // When a specific adlist is requested, skip domains that don't + // belong to it. We check gravity.db here (outside SHM lock would + // be cleaner, but the lock is re-acquired above — safe to call + // gravityDB helpers while holding SHM lock as they use their own + // SQLite handle). + if(adlist_id >= 0 && !gravityDB_domain_in_adlist(domain, adlist_id)) + skip_domain = true; + if(skip_domain || top_domains[i].count < 1) continue; @@ -333,7 +344,9 @@ int api_stats_top_domains(struct ftl_conn *api) { bool blocked = false; // Can be overwritten by query string int count = 10; - // /api/stats/top_domains?blocked=true + // adlist_id < 0 means "all lists" (default / no filter) + int adlist_id = -1; + // /api/stats/top_domains?blocked=true&count=N&list= if(api->request->query_string != NULL) { // Should blocked domains be shown? @@ -342,9 +355,12 @@ int api_stats_top_domains(struct ftl_conn *api) // Does the user request a non-default number of replies? // Note: We do not accept zero query requests here get_int_var(api->request->query_string, "count", &count); + + // Optional filter: restrict results to a specific subscription list + get_int_var(api->request->query_string, "list", &adlist_id); } - cJSON *json = get_top_domains(api, count, blocked, false); + cJSON *json = get_top_domains(api, count, blocked, false, adlist_id); JSON_SEND_OBJECT(json); } diff --git a/src/database/gravity-db.c b/src/database/gravity-db.c index 478353b5da..e18103c756 100644 --- a/src/database/gravity-db.c +++ b/src/database/gravity-db.c @@ -1179,6 +1179,62 @@ static enum db_result domain_in_list(const char *domain, sqlite3_stmt *stmt, con return (rc == SQLITE_ROW) ? FOUND : NOT_FOUND; } +// Check whether a given domain appears in the gravity table for a specific +// adlist. Returns true when the domain is found, false otherwise (including +// when the gravity database is not available). +bool gravityDB_domain_in_adlist(const char *domain, const int adlist_id) +{ + // Open gravity database if not already open + if(!gravityDB_opened && !gravityDB_open()) + { + log_warn("gravityDB_domain_in_adlist(\"%s\", %d): Gravity database not available", + domain, adlist_id); + return false; + } + + sqlite3_stmt *stmt = NULL; + const char *querystr = "SELECT EXISTS(SELECT 1 FROM gravity WHERE domain = ?1 AND adlist_id = ?2);"; + int rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &stmt, NULL); + if(rc != SQLITE_OK) + { + log_err("gravityDB_domain_in_adlist(\"%s\", %d): Failed to prepare statement: %s", + domain, adlist_id, sqlite3_errstr(rc)); + return false; + } + + // Bind domain + if((rc = sqlite3_bind_text(stmt, 1, domain, -1, SQLITE_STATIC)) != SQLITE_OK) + { + log_err("gravityDB_domain_in_adlist(\"%s\", %d): Failed to bind domain: %s", + domain, adlist_id, sqlite3_errstr(rc)); + sqlite3_finalize(stmt); + return false; + } + + // Bind adlist_id + if((rc = sqlite3_bind_int(stmt, 2, adlist_id)) != SQLITE_OK) + { + log_err("gravityDB_domain_in_adlist(\"%s\", %d): Failed to bind adlist_id: %s", + domain, adlist_id, sqlite3_errstr(rc)); + sqlite3_finalize(stmt); + return false; + } + + // Execute + rc = sqlite3_step(stmt); + bool found = false; + if(rc == SQLITE_ROW) + found = sqlite3_column_int(stmt, 0) != 0; + else if(rc != SQLITE_DONE) + log_err("gravityDB_domain_in_adlist(\"%s\", %d): Step failed: %s", + domain, adlist_id, sqlite3_errstr(rc)); + + sqlite3_finalize(stmt); + log_debug(DEBUG_DATABASE, "gravityDB_domain_in_adlist(\"%s\", %d): %s", + domain, adlist_id, found ? "found" : "not found"); + return found; +} + void gravityDB_reload_groups(clientsData *client) { // Rebuild client table statements (possibly from a different group set) diff --git a/src/database/gravity-db.h b/src/database/gravity-db.h index 4f7e3da188..b93690939c 100644 --- a/src/database/gravity-db.h +++ b/src/database/gravity-db.h @@ -77,4 +77,6 @@ bool gravityDB_edit_groups(const enum gravity_list_type listtype, cJSON *groups, time_t gravity_last_updated(void) __attribute__((pure)); +bool gravityDB_domain_in_adlist(const char *domain, const int adlist_id); + #endif //GRAVITY_H