]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] Extend /bayes/classifiers endpoint with metadata
authorAlexander Moisseev <moiseev@mezonplus.ru>
Sat, 21 Feb 2026 06:43:54 +0000 (09:43 +0300)
committerAlexander Moisseev <moiseev@mezonplus.ru>
Sat, 21 Feb 2026 07:47:19 +0000 (10:47 +0300)
- Return classifier metadata (type, per_user, classes) instead of just names array.

Old format: ["bayes", "bayes_multi"]
New format: {classifiers: [{name, type, per_user, classes: [...]}]}

- Export rspamd_classifier_is_per_user() and rspamd_classifier_type()
helper functions to stat_api.h for reuse.

src/controller.c
src/libstat/stat_api.h
src/libstat/stat_process.c

index ebd309798ba18a1cde5c0ebfb64d7b77e0d2827b..1d698647aa7fbaf88f802c9e7d5399d4da927fc4 100644 (file)
@@ -3561,8 +3561,9 @@ rspamd_controller_handle_lua_plugin(struct rspamd_http_connection_entry *conn_en
  * Bayes classifier list command handler:
  * request: /bayes/classifiers
  * headers: Password
- * reply: JSON array of Bayes classifier names
- *   Note: list is in reverse of declaration order (GList prepend).
+ * reply: JSON object with classifiers array containing metadata:
+ *   {classifiers: [{name, type, per_user, classes: [...]}, ...]}
+ *   Classifiers are listed in declaration order from config.
  */
 static int
 rspamd_controller_handle_bayes_classifiers(struct rspamd_http_connection_entry *conn_ent,
@@ -3570,24 +3571,64 @@ rspamd_controller_handle_bayes_classifiers(struct rspamd_http_connection_entry *
 {
        struct rspamd_controller_session *session = conn_ent->ud;
        struct rspamd_controller_worker_ctx *ctx = session->ctx;
-       ucl_object_t *arr;
+       ucl_object_t *result, *classifiers_array, *classifier_obj, *classes_array;
        struct rspamd_classifier_config *clc;
-       GList *cur;
+       GList *cur, *st_cur;
+       GHashTable *classes_seen;
 
        if (!rspamd_controller_check_password(conn_ent, session, msg, FALSE)) {
                return 0;
        }
 
-       arr = ucl_object_typed_new(UCL_ARRAY);
+       result = ucl_object_typed_new(UCL_OBJECT);
+       classifiers_array = ucl_object_typed_new(UCL_ARRAY);
+
+       /*
+        * Iterate backwards to compensate for g_list_prepend() in config parser,
+        * returning classifiers in declaration order from config file.
+        */
        cur = g_list_last(ctx->cfg->classifiers);
        while (cur) {
                clc = cur->data;
-               ucl_array_append(arr, ucl_object_fromstring(clc->name));
+
+               classifier_obj = ucl_object_typed_new(UCL_OBJECT);
+               ucl_object_insert_key(classifier_obj,
+                               ucl_object_fromstring(clc->name),
+                               "name", 0, false);
+               ucl_object_insert_key(classifier_obj,
+                               ucl_object_fromstring(rspamd_classifier_type(clc)),
+                               "type", 0, false);
+               ucl_object_insert_key(classifier_obj,
+                               ucl_object_frombool(rspamd_classifier_is_per_user(clc)),
+                               "per_user", 0, false);
+
+               /* Collect unique class names from statfiles */
+               classes_array = ucl_object_typed_new(UCL_ARRAY);
+               /* Hash table stores borrowed pointers from config, no destroy function needed */
+               classes_seen = g_hash_table_new(rspamd_str_hash, rspamd_str_equal);
+
+               st_cur = clc->statfiles;
+               while (st_cur) {
+                       struct rspamd_statfile_config *stcf = st_cur->data;
+                       if (stcf->class_name && !g_hash_table_contains(classes_seen, stcf->class_name)) {
+                               ucl_array_append(classes_array, ucl_object_fromstring(stcf->class_name));
+                               g_hash_table_add(classes_seen, stcf->class_name);
+                       }
+                       st_cur = g_list_next(st_cur);
+               }
+
+               g_hash_table_unref(classes_seen);
+
+               ucl_object_insert_key(classifier_obj, classes_array, "classes", 0, false);
+               ucl_array_append(classifiers_array, classifier_obj);
+
                cur = g_list_previous(cur);
        }
 
-       rspamd_controller_send_ucl(conn_ent, arr);
-       ucl_object_unref(arr);
+       ucl_object_insert_key(result, classifiers_array, "classifiers", 0, false);
+
+       rspamd_controller_send_ucl(conn_ent, result);
+       ucl_object_unref(result);
        return 0;
 }
 
index aa6111a8b2dfccf612f1883b25205292994a58f1..28726fcddf384d6a0c81ba4774890e94c69a3583 100644 (file)
@@ -135,6 +135,20 @@ rspamd_stat_result_t rspamd_stat_statistics(struct rspamd_task *task,
                                                                                        uint64_t *total_learns,
                                                                                        ucl_object_t **res);
 
+/**
+ * Check if classifier is configured for per-user statistics
+ * @param cfg classifier configuration
+ * @return TRUE if per-user mode is enabled
+ */
+gboolean rspamd_classifier_is_per_user(const struct rspamd_classifier_config *cfg);
+
+/**
+ * Determine classifier type based on its configuration
+ * @param cfg classifier configuration
+ * @return "binary" for binary classifiers, "multi-class" for multiclass
+ */
+const char *rspamd_classifier_type(const struct rspamd_classifier_config *cfg);
+
 void rspamd_stat_unload(void);
 
 /**
index 655e0bdba84290477a8215ddebdfd2af55e31c48..e510d682143c6e3f1f1aba660bb3f604aa37d831 100644 (file)
@@ -1738,7 +1738,7 @@ rspamd_stat_check_autolearn(struct rspamd_task *task)
        return ret;
 }
 
-static gboolean
+gboolean
 rspamd_classifier_is_per_user(const struct rspamd_classifier_config *cfg)
 {
        const ucl_object_t *users_enabled;
@@ -1760,7 +1760,7 @@ rspamd_classifier_is_per_user(const struct rspamd_classifier_config *cfg)
        return TRUE;
 }
 
-static const char *
+const char *
 rspamd_classifier_type(const struct rspamd_classifier_config *cfg)
 {
        gboolean has_spam = FALSE;