// SPDX-License-Identifier: GPL-3.0-or-later
/*
* $Id: dbus.c,v 1.1 2026/05/08 03:23:59 asm Exp $
*
* dbus.c - D-Bus interface for GNUtrition
*
* Copyright (C) 2026 Free Software Foundation, Inc.
*
* Author: Jason Self <jself@gnu.org>
* Anton McClure <asm@gnu.org>
*/
#include "dbus.h"
#include "budget.h"
#include "db.h"
#include "log.h"
#include "i18n.h"
#include <string.h>
/* D-Bus introspection XML for org.gnu.gnutrition.Manager. */
static const gchar introspection_xml[] =
"<node>"
" <interface name='org.gnu.gnutrition.Manager'>"
" <method name='SearchFoods'>"
" <arg type='s' name='query' direction='in'/>"
" <arg type='a(is)' name='results' direction='out'/>"
" </method>"
" <method name='GetNutrientInfo'>"
" <arg type='i' name='food_code' direction='in'/>"
" <arg type='a{sd}' name='nutrients' direction='out'/>"
" </method>"
" <method name='GetFPEDInfo'>"
" <arg type='i' name='food_code' direction='in'/>"
" <arg type='a{sd}' name='fped' direction='out'/>"
" </method>"
" <method name='LogFood'>"
" <arg type='i' name='food_code' direction='in'/>"
" <arg type='d' name='quantity' direction='in'/>"
" <arg type='s' name='date' direction='in'/>"
" <arg type='b' name='success' direction='out'/>"
" </method>"
" <method name='GetLogForDate'>"
" <arg type='s' name='date' direction='in'/>"
" <arg type='a(ids)' name='entries' direction='out'/>"
" </method>"
" <method name='GetBudgetStatus'>"
" <arg type='s' name='date' direction='in'/>"
" <arg type='a{s(dd)}' name='status' direction='out'/>"
" </method>"
" <method name='SetUserProfile'>"
" <arg type='i' name='age' direction='in'/>"
" <arg type='d' name='height' direction='in'/>"
" <arg type='d' name='weight' direction='in'/>"
" <arg type='i' name='activity' direction='in'/>"
" <arg type='i' name='gender' direction='in'/>"
" <arg type='i' name='calorie_target' direction='out'/>"
" </method>"
" <method name='GetUserProfile'>"
" <arg type='(iddii)' name='profile' direction='out'/>"
" </method>"
" <method name='SetCalorieTarget'>"
" <arg type='i' name='calories' direction='in'/>"
" </method>"
" <method name='DeleteLogEntry'>"
" <arg type='i' name='entry_id' direction='in'/>"
" <arg type='b' name='success' direction='out'/>"
" </method>"
" <method name='UpdateLogEntry'>"
" <arg type='i' name='entry_id' direction='in'/>"
" <arg type='d' name='quantity' direction='in'/>"
" <arg type='s' name='date' direction='in'/>"
" <arg type='b' name='success' direction='out'/>"
" </method>"
" </interface>"
"</node>";
static GDBusNodeInfo *introspection_data = NULL;
/* Handle a SearchFoods call. */
static void
handle_search_foods (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
const gchar *query;
struct food_list results;
GVariantBuilder builder;
size_t i;
g_variant_get (parameters, "(&s)", &query);
if (db_search_foods (ctx->food_db, query, &results) < 0)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Database search failed");
return;
}
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(is)"));
for (i = 0; i < results.count; i++)
g_variant_builder_add (&builder, "(is)",
results.items[i].food_code,
results.items[i].description);
food_list_free (&results);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(a(is))", &builder));
}
/* Handle a GetNutrientInfo call. */
static void
handle_get_nutrient_info (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint food_code;
struct nutrient_list nutrients;
GVariantBuilder builder;
size_t i;
g_variant_get (parameters, "(i)", &food_code);
if (db_get_nutrients (ctx->food_db, food_code, &nutrients) < 0)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Nutrient lookup failed");
return;
}
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sd}"));
for (i = 0; i < nutrients.count; i++)
g_variant_builder_add (&builder, "{sd}",
nutrients.items[i].name,
nutrients.items[i].value);
nutrient_list_free (&nutrients);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(a{sd})", &builder));
}
/* Handle a GetFPEDInfo call. */
static void
handle_get_fped_info (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint food_code;
struct fped_entry fped;
GVariantBuilder builder;
int rc;
g_variant_get (parameters, "(i)", &food_code);
rc = db_get_fped (ctx->food_db, food_code, &fped);
if (rc < 0)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"FPED lookup failed");
return;
}
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sd}"));
if (rc == 0)
{
g_variant_builder_add (&builder, "{sd}", "Vegetables",
fped.vegetables);
g_variant_builder_add (&builder, "{sd}", "Fruits", fped.fruits);
g_variant_builder_add (&builder, "{sd}", "Grains", fped.grains);
g_variant_builder_add (&builder, "{sd}", "Dairy", fped.dairy);
g_variant_builder_add (&builder, "{sd}", "Protein", fped.protein);
g_variant_builder_add (&builder, "{sd}", "Oils", fped.oils);
}
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(a{sd})", &builder));
}
/* Handle a LogFood call. */
static void
handle_log_food (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint food_code;
gdouble quantity;
const gchar *date;
struct food_list results;
const char *desc;
size_t j;
gboolean success;
g_variant_get (parameters, "(ids)", &food_code, &quantity, &date);
/* Look up food description. */
desc = _("Unknown food");
if (db_search_foods (ctx->food_db, "", &results) == 0)
{
for (j = 0; j < results.count; j++)
{
if (results.items[j].food_code == food_code)
{
desc = results.items[j].description;
break;
}
}
}
success = (log_add (ctx->log_db, food_code, desc, date, quantity) == 0);
food_list_free (&results);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(b)", success));
}
/* Handle a GetLogForDate call. */
static void
handle_get_log_for_date (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
const gchar *date;
struct log_list entries;
GVariantBuilder builder;
size_t i;
g_variant_get (parameters, "(&s)", &date);
if (log_get_day (ctx->log_db, date, &entries) < 0)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Log retrieval failed");
return;
}
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ids)"));
for (i = 0; i < entries.count; i++)
g_variant_builder_add (&builder, "(ids)",
entries.items[i].food_code,
entries.items[i].servings,
entries.items[i].description);
log_list_free (&entries);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(a(ids))", &builder));
}
/* Handle a GetBudgetStatus call. */
static void
handle_get_budget_status (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
const gchar *date;
struct daily_budget budget;
struct daily_budget consumed;
struct log_list entries;
GVariantBuilder builder;
size_t i;
g_variant_get (parameters, "(&s)", &date);
budget = budget_for_calories (ctx->calories);
memset (&consumed, 0, sizeof consumed);
if (log_get_day (ctx->log_db, date, &entries) == 0)
{
for (i = 0; i < entries.count; i++)
{
struct fped_entry fped;
if (db_get_fped (ctx->food_db, entries.items[i].food_code,
&fped) == 0)
{
double s = entries.items[i].servings;
consumed.vegetables += fped.vegetables * s;
consumed.fruits += fped.fruits * s;
consumed.grains += fped.grains * s;
consumed.dairy += fped.dairy * s;
consumed.protein += fped.protein * s;
consumed.oils += fped.oils * s;
}
}
log_list_free (&entries);
}
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{s(dd)}"));
g_variant_builder_add (&builder, "{s(dd)}", "Vegetables",
budget.vegetables, consumed.vegetables);
g_variant_builder_add (&builder, "{s(dd)}", "Fruits",
budget.fruits, consumed.fruits);
g_variant_builder_add (&builder, "{s(dd)}", "Grains",
budget.grains, consumed.grains);
g_variant_builder_add (&builder, "{s(dd)}", "Dairy",
budget.dairy, consumed.dairy);
g_variant_builder_add (&builder, "{s(dd)}", "Protein",
budget.protein, consumed.protein);
g_variant_builder_add (&builder, "{s(dd)}", "Oils",
budget.oils, consumed.oils);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(a{s(dd)})", &builder));
}
/* Handle a SetUserProfile call. */
static void
handle_set_user_profile (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint age, activity, gender;
gdouble height, weight;
struct user_profile prof;
g_variant_get (parameters, "(iddi)", &age, &height, &weight, &activity, &gender);
prof.age_years = age;
prof.height_cm = height;
prof.weight_kg = weight;
prof.activity_level = activity;
prof.gender = gender;
prof.calorie_target = budget_estimate_calories (age, height, weight,
activity, gender);
if (log_save_profile (ctx->log_db, &prof) < 0)
{
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Failed to save profile");
return;
}
ctx->calories = prof.calorie_target;
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(i)", prof.calorie_target));
}
/* Handle a GetUserProfile call. */
static void
handle_get_user_profile (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
struct user_profile prof;
int rc;
(void) parameters;
rc = log_get_profile (ctx->log_db, &prof);
if (rc != 0)
{
/* No profile saved yet; return zeros. */
memset (&prof, 0, sizeof prof);
}
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("((iddii))",
prof.age_years,
prof.height_cm,
prof.weight_kg,
prof.activity_level,
prof.calorie_target));
}
/* Handle a SetCalorieTarget call. */
static void
handle_set_calorie_target (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint cal;
g_variant_get (parameters, "(i)", &cal);
ctx->calories = budget_round_to_pattern (cal);
g_dbus_method_invocation_return_value (invocation, NULL);
}
/* Handle a DeleteLogEntry call. */
static void
handle_delete_log_entry (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint entry_id;
gboolean success;
g_variant_get (parameters, "(i)", &entry_id);
success = (log_delete (ctx->log_db, entry_id) == 0);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(b)", success));
}
/* Handle an UpdateLogEntry call. */
static void
handle_update_log_entry (struct dbus_context *ctx,
GVariant *parameters,
GDBusMethodInvocation *invocation)
{
gint entry_id;
gdouble quantity;
const gchar *date;
gboolean success;
g_variant_get (parameters, "(ids)", &entry_id, &quantity, &date);
success = (log_update (ctx->log_db, entry_id, date, quantity) == 0);
g_dbus_method_invocation_return_value (invocation,
g_variant_new ("(b)", success));
}
/* Dispatch incoming D-Bus method calls. */
static void
handle_method_call (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
struct dbus_context *ctx = user_data;
(void) connection;
(void) sender;
(void) object_path;
(void) interface_name;
if (g_strcmp0 (method_name, "SearchFoods") == 0)
handle_search_foods (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "GetNutrientInfo") == 0)
handle_get_nutrient_info (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "GetFPEDInfo") == 0)
handle_get_fped_info (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "LogFood") == 0)
handle_log_food (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "GetLogForDate") == 0)
handle_get_log_for_date (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "GetBudgetStatus") == 0)
handle_get_budget_status (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "SetUserProfile") == 0)
handle_set_user_profile (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "GetUserProfile") == 0)
handle_get_user_profile (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "SetCalorieTarget") == 0)
handle_set_calorie_target (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "DeleteLogEntry") == 0)
handle_delete_log_entry (ctx, parameters, invocation);
else if (g_strcmp0 (method_name, "UpdateLogEntry") == 0)
handle_update_log_entry (ctx, parameters, invocation);
else
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD,
"Unknown method: %s", method_name);
}
static const GDBusInterfaceVTable interface_vtable =
{
handle_method_call,
NULL, /* get_property */
NULL, /* set_property */
{ NULL }
};
/* Called when the bus name is acquired. Register our object. */
static void
on_bus_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
GError *error = NULL;
(void) name;
g_dbus_connection_register_object (connection,
"/org/gnu/gnutrition/Manager",
introspection_data->interfaces[0],
&interface_vtable,
user_data, NULL, &error);
if (error)
{
g_printerr ("gnutrition: D-Bus register error: %s\n", error->message);
g_error_free (error);
}
}
static void
on_name_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
(void) connection;
(void) name;
(void) user_data;
}
static void
on_name_lost (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
(void) connection;
(void) user_data;
g_printerr ("gnutrition: lost D-Bus name '%s'\n", name);
}
guint
dbus_service_start (struct dbus_context *ctx)
{
guint owner_id;
introspection_data = g_dbus_node_info_new_for_xml (introspection_xml,
NULL);
if (!introspection_data)
{
g_printerr ("gnutrition: failed to parse D-Bus introspection XML\n");
return 0;
}
owner_id = g_bus_own_name (G_BUS_TYPE_SESSION,
"org.gnu.gnutrition",
G_BUS_NAME_OWNER_FLAGS_NONE,
on_bus_acquired,
on_name_acquired,
on_name_lost,
ctx, NULL);
return owner_id;
}
void
dbus_service_stop (guint owner_id)
{
if (owner_id > 0)
g_bus_unown_name (owner_id);
if (introspection_data)
{
g_dbus_node_info_unref (introspection_data);
introspection_data = NULL;
}
}
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>