feat(person): add investigation roles and graph colors

This commit is contained in:
grayTerminal-sh 2026-07-22 18:29:34 +02:00
parent 8d5a5910db
commit 205c65f275
22 changed files with 478 additions and 8 deletions

View file

@ -165,6 +165,8 @@ Le socle actuel comprend notamment :
conservant leur lisibilité pendant le zoom ; conservant leur lisibilité pendant le zoom ;
- création de personnes observées avec statut d'identification, confiance, - création de personnes observées avec statut d'identification, confiance,
notes factuelles et rattachement facultatif à une preuve ; notes factuelles et rattachement facultatif à une preuve ;
- catégories d'enquête des personnes en SQLite V5, modifiables depuis leur
fiche et représentées par une couleur et un libellé dans le graphe ;
- relations représentées par des flèches directes à libellé cliquable, sans - relations représentées par des flèches directes à libellé cliquable, sans
ajouter de faux nœud visuel entre les entités ; ajouter de faux nœud visuel entre les entités ;
- historique OSINT contextuel en lecture seule avec détail des exécutions, - historique OSINT contextuel en lecture seule avec détail des exécutions,

View file

@ -1,7 +1,7 @@
/****************************************************************************** /******************************************************************************
* Labfy Investigation * Labfy Investigation
* *
* Extensions idempotentes du schéma SQLite courant V4 * Extensions idempotentes du schéma SQLite courant V5
******************************************************************************/ ******************************************************************************/
/* /*

30
database/schema_v5.sql Normal file
View file

@ -0,0 +1,30 @@
/******************************************************************************
* Labfy Investigation
*
* Migration du schéma SQLite V4 vers V5 : rôles d'enquête des personnes
******************************************************************************/
CREATE TABLE person_roles
(
entity_id TEXT PRIMARY KEY,
role TEXT NOT NULL DEFAULT 'uncategorized',
updated_at TEXT NOT NULL,
FOREIGN KEY (entity_id)
REFERENCES entites(id)
ON UPDATE CASCADE
ON DELETE CASCADE,
CHECK (role IN
(
'uncategorized',
'alleged_scammer',
'victim',
'witness',
'suspect',
'related_person'
)),
CHECK (length(updated_at) = 20)
);
CREATE INDEX idx_person_roles_role ON person_roles(role);

View file

@ -1161,6 +1161,17 @@ compte via `preuve_entites`. La création des deux lignes et de cette liaison
est transactionnelle : aucun nœud incomplet n'est conservé si une étape est transactionnelle : aucun nœud incomplet n'est conservé si une étape
échoue. échoue.
### Rôles d'enquête des personnes — schéma V5
La table `person_roles` associe une entité de type personne à une catégorie
d'enquête contrôlée : scammer présumé, victime, témoin, suspect, personne liée
ou non catégorisée. La clé étrangère avec suppression en cascade évite les
rôles orphelins. Le code stable est persisté ; le libellé et la couleur restent
des choix de présentation.
Une personne sans ligne dans cette table est toujours interprétée comme non
catégorisée, ce qui garantit la compatibilité avec les enquêtes antérieures.
--- ---
# 6. Tables de liaison # 6. Tables de liaison

View file

@ -5,6 +5,7 @@
#ifndef LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H #ifndef LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H
#define LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H #define LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H
#include "database/database.h" #include "database/database.h"
#include "models/entity_record.h"
#include <glib.h> #include <glib.h>
G_BEGIN_DECLS G_BEGIN_DECLS
/** @brief Données factuelles utilisées pour créer une personne. */ /** @brief Données factuelles utilisées pour créer une personne. */
@ -28,5 +29,8 @@ typedef struct
*/ */
gboolean person_entity_service_create(Database *database, gboolean person_entity_service_create(Database *database,
const PersonEntityInput *input, char **out_identifier, GError **error); const PersonEntityInput *input, char **out_identifier, GError **error);
/** @brief Enregistre le rôle d'une personne existante. */
gboolean person_entity_service_update_role(Database *database,
const char *entity_identifier, PersonRole role, GError **error);
G_END_DECLS G_END_DECLS
#endif #endif

View file

@ -81,6 +81,9 @@ bool schema_install_v4(
Database *database Database *database
); );
/** @brief Installe la migration des rôles de personnes du schéma V5. */
bool schema_install_v5(Database *database);
/** /**
* @brief Garantit la présence des extensions du schéma courant V2. * @brief Garantit la présence des extensions du schéma courant V2.
* *

View file

@ -29,6 +29,17 @@ typedef enum
ENTITY_STATUS_DELETED ENTITY_STATUS_DELETED
} EntityStatus; } EntityStatus;
/** @brief Rôle d'une personne dans l'enquête. */
typedef enum
{
PERSON_ROLE_UNCATEGORIZED,
PERSON_ROLE_ALLEGED_SCAMMER,
PERSON_ROLE_VICTIM,
PERSON_ROLE_WITNESS,
PERSON_ROLE_SUSPECT,
PERSON_ROLE_RELATED_PERSON
} PersonRole;
/** /**
* @brief Codes d'erreur produits par EntityRecord. * @brief Codes d'erreur produits par EntityRecord.
*/ */
@ -170,6 +181,18 @@ EntityStatus entity_record_get_status(
const EntityRecord *entity_record const EntityRecord *entity_record
); );
/** @brief Affecte un rôle d'enquête à une entité de type personne. */
gboolean entity_record_set_person_role(EntityRecord *entity_record,
PersonRole role);
/** @brief Retourne le rôle d'enquête, non catégorisé par défaut. */
PersonRole entity_record_get_person_role(const EntityRecord *entity_record);
/** @brief Convertit un rôle en code stable pour SQLite. */
const char *person_role_to_code(PersonRole role);
/** @brief Convertit un code SQLite en rôle. */
PersonRole person_role_from_code(const char *code);
/** @brief Retourne le libellé français d'un rôle. */
const char *person_role_get_label(PersonRole role);
G_END_DECLS G_END_DECLS
#endif #endif

View file

@ -143,6 +143,10 @@ typedef void (*MainWindowEditRelationCallback)(
gpointer user_data gpointer user_data
); );
/** @brief Callback appelé pour catégoriser une personne. */
typedef void (*MainWindowPersonRoleCallback)(const char *entity_identifier,
PersonRole role, gpointer user_data);
/** /**
* @brief Callback appelé lors du déclenchement d'une action OSINT. * @brief Callback appelé lors du déclenchement d'une action OSINT.
* *
@ -240,6 +244,10 @@ void main_window_set_edit_relation_callback(
gpointer user_data gpointer user_data
); );
/** @brief Définit le callback de catégorisation d'une personne. */
void main_window_set_person_role_callback(MainWindow *main_window,
MainWindowPersonRoleCallback callback, gpointer user_data);
/** /**
* @brief Définit le callback de déclenchement des actions OSINT. * @brief Définit le callback de déclenchement des actions OSINT.
* *
@ -437,6 +445,9 @@ gboolean main_window_select_graph_relation(
MainWindow *main_window, MainWindow *main_window,
const char *relation_identifier const char *relation_identifier
); );
/** @brief Sélectionne une entité dans le graphe affiché. */
gboolean main_window_select_graph_entity(MainWindow *main_window,
const char *entity_identifier);
/** /**
* @brief Affiche une erreur de chargement du graphe. * @brief Affiche une erreur de chargement du graphe.

View file

@ -7,6 +7,7 @@
#define LABFY_INVESTIGATION_ENTITY_DETAILS_PANEL_H #define LABFY_INVESTIGATION_ENTITY_DETAILS_PANEL_H
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include "models/entity_record.h"
G_BEGIN_DECLS G_BEGIN_DECLS
@ -45,6 +46,13 @@ typedef void (*EntityDetailsPanelAddRelationCallback)(
gpointer user_data gpointer user_data
); );
/** @brief Callback appelé lors du changement de catégorie d'une personne. */
typedef void (*EntityDetailsPanelPersonRoleCallback)(
const char *entity_identifier,
PersonRole role,
gpointer user_data
);
/** /**
* @brief Crée un volet de détails fermé. * @brief Crée un volet de détails fermé.
* *
@ -120,6 +128,13 @@ void entity_details_panel_set_add_relation_callback(
gpointer user_data gpointer user_data
); );
/** @brief Définit le callback de changement de catégorie d'une personne. */
void entity_details_panel_set_person_role_callback(
EntityDetailsPanel *details_panel,
EntityDetailsPanelPersonRoleCallback callback,
gpointer user_data
);
/** /**
* @brief Indique si le volet est actuellement ouvert. * @brief Indique si le volet est actuellement ouvert.
* *

View file

@ -8,6 +8,7 @@
#include "core/investigation_node.h" #include "core/investigation_node.h"
#include "models/evidence_record.h" #include "models/evidence_record.h"
#include "models/entity_record.h"
#include "models/osint_action_catalog.h" #include "models/osint_action_catalog.h"
#include <gtk/gtk.h> #include <gtk/gtk.h>
@ -84,6 +85,10 @@ typedef void (*WorkspaceEditRelationCallback)(
gpointer user_data gpointer user_data
); );
/** @brief Callback appelé pour catégoriser une personne. */
typedef void (*WorkspacePersonRoleCallback)(const char *entity_identifier,
PersonRole role, gpointer user_data);
/** /**
* @brief Callback appelé lors du déclenchement d'une action OSINT. * @brief Callback appelé lors du déclenchement d'une action OSINT.
* *
@ -301,6 +306,10 @@ void workspace_set_edit_relation_callback(
gpointer user_data gpointer user_data
); );
/** @brief Définit le callback de catégorisation d'une personne. */
void workspace_set_person_role_callback(Workspace *workspace,
WorkspacePersonRoleCallback callback, gpointer user_data);
/** /**
* @brief Affiche l'état de chargement du graphe. * @brief Affiche l'état de chargement du graphe.
* *

View file

@ -126,6 +126,7 @@ struct Application
char *selected_evidence_identifier; char *selected_evidence_identifier;
char *pending_relation_selection_identifier; char *pending_relation_selection_identifier;
char *pending_entity_selection_identifier;
}; };
/** /**
@ -747,6 +748,13 @@ static void application_on_graph_loaded(
g_clear_pointer(&application->pending_relation_selection_identifier, g_clear_pointer(&application->pending_relation_selection_identifier,
g_free); g_free);
} }
if (application->pending_entity_selection_identifier != NULL)
{
main_window_select_graph_entity(application->main_window,
application->pending_entity_selection_identifier);
g_clear_pointer(&application->pending_entity_selection_identifier,
g_free);
}
status_message = status_message =
g_strdup_printf( g_strdup_printf(
@ -4935,6 +4943,35 @@ typedef struct
char *relation_identifier; char *relation_identifier;
} ApplicationEditRelationContext; } ApplicationEditRelationContext;
/** @brief Enregistre la catégorie choisie depuis la fiche personne. */
static void application_on_person_role_changed(const char *entity_identifier,
PersonRole role, gpointer user_data)
{
Application *application = user_data;
Database *database = NULL;
const InvestigationProject *project = NULL;
GError *error = NULL;
if (application == NULL || application->session == NULL) return;
database = investigation_session_get_database(application->session);
project = investigation_session_get_project(application->session);
if (!person_entity_service_update_role(database, entity_identifier,
role, &error))
{
application_present_error(application, "Catégorie non enregistrée",
error != NULL ? error->message :
"Impossible de catégoriser cette personne.");
g_clear_error(&error);
return;
}
g_free(application->pending_entity_selection_identifier);
application->pending_entity_selection_identifier =
g_strdup(entity_identifier);
main_window_set_status(application->main_window,
"Catégorie enregistrée. Actualisation du graphe…");
application_start_graph_loading(application,
investigation_project_get_database_path(project));
}
/** @brief Libère le contexte d'édition d'une relation. */ /** @brief Libère le contexte d'édition d'une relation. */
static void application_edit_relation_context_free( static void application_edit_relation_context_free(
ApplicationEditRelationContext *context) ApplicationEditRelationContext *context)
@ -6476,6 +6513,8 @@ static void application_on_activate(
main_window_set_edit_relation_callback(application->main_window, main_window_set_edit_relation_callback(application->main_window,
application_on_edit_relation_requested, application); application_on_edit_relation_requested, application);
main_window_set_person_role_callback(application->main_window,
application_on_person_role_changed, application);
main_window_set_osint_action_callback( main_window_set_osint_action_callback(
application->main_window, application->main_window,
@ -6752,6 +6791,7 @@ void application_free(
); );
g_clear_pointer(&application->pending_relation_selection_identifier, g_clear_pointer(&application->pending_relation_selection_identifier,
g_free); g_free);
g_clear_pointer(&application->pending_entity_selection_identifier, g_free);
g_free( g_free(
application application

View file

@ -6,6 +6,7 @@
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/evidence_entity_dao.h" #include "dao/evidence_entity_dao.h"
#include "database/transaction.h" #include "database/transaction.h"
#include "database/statement.h"
#include "models/entity_record.h" #include "models/entity_record.h"
/** @brief Indique si le niveau d'identification est reconnu. */ /** @brief Indique si le niveau d'identification est reconnu. */
@ -16,6 +17,58 @@ static gboolean person_entity_service_status_valid(const char *status)
g_strcmp0(status, "confirmed") == 0; g_strcmp0(status, "confirmed") == 0;
} }
gboolean person_entity_service_update_role(Database *database,
const char *entity_identifier, PersonRole role, GError **error)
{
static const char validate_sql[] =
"SELECT 1 FROM entites e JOIN types_entite t ON t.id=e.type_id "
"WHERE e.id=? AND t.code='person';";
static const char upsert_sql[] =
"INSERT INTO person_roles(entity_id, role, updated_at) VALUES(?,?,?) "
"ON CONFLICT(entity_id) DO UPDATE SET role=excluded.role, "
"updated_at=excluded.updated_at;";
DatabaseStatement *statement = NULL;
GDateTime *now = NULL;
char *timestamp = NULL;
const char *role_code = person_role_to_code(role);
gboolean active = FALSE, success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (database == NULL || entity_identifier == NULL ||
!g_uuid_string_is_valid(entity_identifier) || role_code == NULL)
goto invalid;
statement = database_statement_prepare(database, validate_sql);
if (statement == NULL || !database_statement_bind_text(statement, 1,
entity_identifier) || database_statement_step(statement) !=
DATABASE_STATEMENT_STEP_ROW)
goto invalid;
database_statement_finalize(statement); statement = NULL;
now = g_date_time_new_now_utc();
timestamp = now != NULL ? g_date_time_format(now,
"%Y-%m-%dT%H:%M:%SZ") : NULL;
if (timestamp == NULL || !database_transaction_begin(database))
goto cleanup;
active = TRUE;
statement = database_statement_prepare(database, upsert_sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, entity_identifier) ||
!database_statement_bind_text(statement, 2, role_code) ||
!database_statement_bind_text(statement, 3, timestamp) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
goto cleanup;
database_statement_finalize(statement); statement = NULL;
if (!database_transaction_commit(database)) goto cleanup;
active = FALSE; success = TRUE; goto cleanup;
invalid:
g_set_error_literal(error, g_quark_from_static_string(
"person-entity-service-error"), 3,
"La personne ou la catégorie sélectionnée est invalide.");
cleanup:
database_statement_finalize(statement);
if (!success && active) database_transaction_rollback(database);
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
return success;
}
gboolean person_entity_service_create(Database *database, gboolean person_entity_service_create(Database *database,
const PersonEntityInput *input, char **out_identifier, GError **error) const PersonEntityInput *input, char **out_identifier, GError **error)
{ {

View file

@ -68,10 +68,12 @@ static const char *const entity_dao_find_by_identifier_sql =
" entites.confiance," " entites.confiance,"
" entites.created_at," " entites.created_at,"
" entites.updated_at," " entites.updated_at,"
" entites.status " " entites.status,"
" person_roles.role "
"FROM entites " "FROM entites "
"LEFT JOIN types_entite " "LEFT JOIN types_entite "
"ON types_entite.id = entites.type_id " "ON types_entite.id = entites.type_id "
"LEFT JOIN person_roles ON person_roles.entity_id = entites.id "
"WHERE entites.id = ?;"; "WHERE entites.id = ?;";
/** /**
@ -87,10 +89,12 @@ static const char *const entity_dao_list_all_sql =
" entites.confiance," " entites.confiance,"
" entites.created_at," " entites.created_at,"
" entites.updated_at," " entites.updated_at,"
" entites.status " " entites.status,"
" person_roles.role "
"FROM entites " "FROM entites "
"LEFT JOIN types_entite " "LEFT JOIN types_entite "
"ON types_entite.id = entites.type_id " "ON types_entite.id = entites.type_id "
"LEFT JOIN person_roles ON person_roles.entity_id = entites.id "
"ORDER BY " "ORDER BY "
" entites.created_at ASC," " entites.created_at ASC,"
" entites.id ASC;"; " entites.id ASC;";
@ -674,6 +678,9 @@ static EntityRecord *entity_dao_read_current_record(
char *status_text = char *status_text =
NULL; NULL;
char *person_role_text =
NULL;
int64_t confidence_value = int64_t confidence_value =
-1; -1;
@ -740,6 +747,11 @@ static EntityRecord *entity_dao_read_current_record(
statement, statement,
8, 8,
&status_text &status_text
) ||
!database_statement_column_text(
statement,
9,
&person_role_text
)) ))
{ {
entity_dao_set_error_literal( entity_dao_set_error_literal(
@ -812,6 +824,11 @@ static EntityRecord *entity_dao_read_current_record(
); );
} }
} }
else if (person_role_text != NULL)
{
entity_record_set_person_role(entity_record,
person_role_from_code(person_role_text));
}
cleanup: cleanup:
@ -823,6 +840,8 @@ cleanup:
status_text status_text
); );
g_free(person_role_text);
g_free( g_free(
updated_at updated_at
); );

View file

@ -16,12 +16,12 @@
/** /**
* @brief Version actuelle du schéma SQLite. * @brief Version actuelle du schéma SQLite.
*/ */
#define DATABASE_SCHEMA_VERSION_CURRENT 4 #define DATABASE_SCHEMA_VERSION_CURRENT 5
/** /**
* @brief Version actuelle sous forme textuelle pour metadata. * @brief Version actuelle sous forme textuelle pour metadata.
*/ */
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "4" #define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "5"
/** /**
* @brief Nom de l'application enregistré dans les métadonnées. * @brief Nom de l'application enregistré dans les métadonnées.
@ -712,6 +712,24 @@ rollback:
return false; return false;
} }
/** @brief Applique atomiquement la migration du schéma V4 vers V5. */
static bool database_migrate_v4_to_v5(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database))
return false;
transaction_started = true;
if (!schema_install_v5(database) ||
!database_update_schema_version(database, "5") ||
!database_transaction_commit(database))
goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V4 vers V5.");
return false;
}
/** /**
* @brief Garantit atomiquement la présence des extensions du schéma courant. * @brief Garantit atomiquement la présence des extensions du schéma courant.
*/ */
@ -910,6 +928,12 @@ bool database_migrate_to_latest(
schema_version = 4; schema_version = 4;
break; break;
case 4:
if (!database_migrate_v4_to_v5(database))
return false;
schema_version = 5;
break;
default: default:
database_set_error( database_set_error(
database, database,
@ -1031,6 +1055,11 @@ bool database_initialize(
goto rollback; goto rollback;
} }
if (!schema_install_v5(database))
{
goto rollback;
}
if (!schema_ensure_current( if (!schema_ensure_current(
database database
)) ))

View file

@ -215,6 +215,12 @@ bool schema_install_v4(
); );
} }
bool schema_install_v5(Database *database)
{
return schema_execute_file(database, "database/schema_v5.sql",
"la migration SQLite V5");
}
bool schema_ensure_current( bool schema_ensure_current(
Database *database Database *database
) )

View file

@ -24,6 +24,7 @@ struct EntityRecord
char *updated_at; char *updated_at;
EntityStatus status; EntityStatus status;
PersonRole person_role;
}; };
/** /**
@ -582,3 +583,62 @@ EntityStatus entity_record_get_status(
? entity_record->status ? entity_record->status
: ENTITY_STATUS_UNKNOWN; : ENTITY_STATUS_UNKNOWN;
} }
gboolean entity_record_set_person_role(EntityRecord *entity_record,
PersonRole role)
{
if (entity_record == NULL ||
g_strcmp0(entity_record->type_identifier, "person") != 0 ||
person_role_to_code(role) == NULL)
{
return FALSE;
}
entity_record->person_role = role;
return TRUE;
}
PersonRole entity_record_get_person_role(const EntityRecord *entity_record)
{
return entity_record != NULL ? entity_record->person_role :
PERSON_ROLE_UNCATEGORIZED;
}
const char *person_role_to_code(PersonRole role)
{
switch (role)
{
case PERSON_ROLE_UNCATEGORIZED: return "uncategorized";
case PERSON_ROLE_ALLEGED_SCAMMER: return "alleged_scammer";
case PERSON_ROLE_VICTIM: return "victim";
case PERSON_ROLE_WITNESS: return "witness";
case PERSON_ROLE_SUSPECT: return "suspect";
case PERSON_ROLE_RELATED_PERSON: return "related_person";
default: return NULL;
}
}
PersonRole person_role_from_code(const char *code)
{
if (g_strcmp0(code, "alleged_scammer") == 0)
return PERSON_ROLE_ALLEGED_SCAMMER;
if (g_strcmp0(code, "victim") == 0) return PERSON_ROLE_VICTIM;
if (g_strcmp0(code, "witness") == 0) return PERSON_ROLE_WITNESS;
if (g_strcmp0(code, "suspect") == 0) return PERSON_ROLE_SUSPECT;
if (g_strcmp0(code, "related_person") == 0)
return PERSON_ROLE_RELATED_PERSON;
return PERSON_ROLE_UNCATEGORIZED;
}
const char *person_role_get_label(PersonRole role)
{
switch (role)
{
case PERSON_ROLE_ALLEGED_SCAMMER: return "Scammer présumé";
case PERSON_ROLE_VICTIM: return "Victime";
case PERSON_ROLE_WITNESS: return "Témoin";
case PERSON_ROLE_SUSPECT: return "Suspect";
case PERSON_ROLE_RELATED_PERSON: return "Personne liée";
case PERSON_ROLE_UNCATEGORIZED:
default: return "Non catégorisé";
}
}

View file

@ -131,6 +131,8 @@ struct MainWindow
MainWindowEditRelationCallback edit_relation_callback; MainWindowEditRelationCallback edit_relation_callback;
gpointer edit_relation_user_data; gpointer edit_relation_user_data;
MainWindowPersonRoleCallback person_role_callback;
gpointer person_role_user_data;
MainWindowOsintActionCallback MainWindowOsintActionCallback
osint_action_callback; osint_action_callback;
@ -526,6 +528,16 @@ static void main_window_on_edit_relation_requested(
main_window->edit_relation_user_data); main_window->edit_relation_user_data);
} }
/** @brief Relaie la catégorisation d'une personne. */
static void main_window_on_person_role_changed(const char *entity_identifier,
PersonRole role, gpointer user_data)
{
MainWindow *main_window = user_data;
if (main_window != NULL && main_window->person_role_callback != NULL)
main_window->person_role_callback(entity_identifier, role,
main_window->person_role_user_data);
}
/** /**
* @brief Relaie la demande de vérification provenant du Workspace. * @brief Relaie la demande de vérification provenant du Workspace.
*/ */
@ -866,6 +878,8 @@ MainWindow *main_window_new(
workspace_set_edit_relation_callback(main_window->workspace, workspace_set_edit_relation_callback(main_window->workspace,
main_window_on_edit_relation_requested, main_window); main_window_on_edit_relation_requested, main_window);
workspace_set_person_role_callback(main_window->workspace,
main_window_on_person_role_changed, main_window);
workspace_set_osint_action_callback( workspace_set_osint_action_callback(
main_window->workspace, main_window->workspace,
@ -1280,6 +1294,14 @@ gboolean main_window_select_graph_relation(MainWindow *main_window,
relation_identifier); relation_identifier);
} }
gboolean main_window_select_graph_entity(MainWindow *main_window,
const char *entity_identifier)
{
if (main_window == NULL) return FALSE;
return workspace_select_graph_entity(main_window->workspace,
entity_identifier);
}
void main_window_set_graph_error( void main_window_set_graph_error(
MainWindow *main_window, MainWindow *main_window,
const char *message const char *message
@ -1650,6 +1672,14 @@ void main_window_set_edit_relation_callback(MainWindow *main_window,
main_window->edit_relation_user_data = user_data; main_window->edit_relation_user_data = user_data;
} }
void main_window_set_person_role_callback(MainWindow *main_window,
MainWindowPersonRoleCallback callback, gpointer user_data)
{
if (main_window == NULL) return;
main_window->person_role_callback = callback;
main_window->person_role_user_data = user_data;
}
void main_window_set_quit_callback( void main_window_set_quit_callback(
MainWindow *main_window, MainWindow *main_window,
MainWindowQuitCallback callback, MainWindowQuitCallback callback,
@ -1803,6 +1833,7 @@ void main_window_free(
workspace_set_edit_relation_callback(main_window->workspace, workspace_set_edit_relation_callback(main_window->workspace,
NULL, NULL); NULL, NULL);
workspace_set_person_role_callback(main_window->workspace, NULL, NULL);
main_window->graph_node_moved_callback = main_window->graph_node_moved_callback =
NULL; NULL;
@ -1824,6 +1855,8 @@ void main_window_free(
main_window->edit_relation_callback = NULL; main_window->edit_relation_callback = NULL;
main_window->edit_relation_user_data = NULL; main_window->edit_relation_user_data = NULL;
main_window->person_role_callback = NULL;
main_window->person_role_user_data = NULL;
main_window->show_graph_callback = main_window->show_graph_callback =
NULL; NULL;

View file

@ -29,6 +29,8 @@ struct EntityDetailsPanel
GtkWidget *entity_created_at_label; GtkWidget *entity_created_at_label;
GtkWidget *entity_updated_at_label; GtkWidget *entity_updated_at_label;
GtkWidget *entity_identifier_label; GtkWidget *entity_identifier_label;
GtkWidget *person_role_box;
GtkDropDown *person_role_dropdown;
char *selected_entity_identifier; char *selected_entity_identifier;
@ -40,8 +42,30 @@ struct EntityDetailsPanel
gpointer gpointer
add_relation_user_data; add_relation_user_data;
EntityDetailsPanelPersonRoleCallback person_role_callback;
gpointer person_role_user_data;
gboolean updating_person_role;
}; };
/** @brief Relaie une catégorie choisie explicitement par l'utilisateur. */
static void entity_details_panel_on_person_role_changed(GObject *object,
GParamSpec *parameter, gpointer user_data)
{
EntityDetailsPanel *details_panel = user_data;
guint selected = GTK_INVALID_LIST_POSITION;
(void) object; (void) parameter;
if (details_panel == NULL || details_panel->updating_person_role ||
details_panel->person_role_callback == NULL ||
details_panel->selected_entity_identifier == NULL)
return;
selected = gtk_drop_down_get_selected(details_panel->person_role_dropdown);
if (selected > PERSON_ROLE_RELATED_PERSON) return;
details_panel->person_role_callback(
details_panel->selected_entity_identifier, (PersonRole) selected,
details_panel->person_role_user_data);
}
/** /**
* @brief Définit une valeur avec un texte de remplacement. * @brief Définit une valeur avec un texte de remplacement.
*/ */
@ -314,6 +338,8 @@ EntityDetailsPanel *entity_details_panel_new(void)
GtkWidget *details_grid = GtkWidget *details_grid =
NULL; NULL;
GtkStringList *person_role_labels = NULL;
details_panel = details_panel =
g_try_new0( g_try_new0(
EntityDetailsPanel, EntityDetailsPanel,
@ -695,6 +721,23 @@ EntityDetailsPanel *entity_details_panel_new(void)
"Identifiant" "Identifiant"
); );
details_panel->person_role_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
person_role_labels = gtk_string_list_new(NULL);
for (guint role = PERSON_ROLE_UNCATEGORIZED;
role <= PERSON_ROLE_RELATED_PERSON; role++)
gtk_string_list_append(person_role_labels,
person_role_get_label((PersonRole) role));
details_panel->person_role_dropdown = GTK_DROP_DOWN(
gtk_drop_down_new(G_LIST_MODEL(person_role_labels), NULL));
person_role_labels = NULL;
gtk_box_append(GTK_BOX(details_panel->person_role_box),
gtk_label_new("Catégorie dans lenquête"));
gtk_box_append(GTK_BOX(details_panel->person_role_box),
GTK_WIDGET(details_panel->person_role_dropdown));
gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box);
g_signal_connect(details_panel->person_role_dropdown, "notify::selected",
G_CALLBACK(entity_details_panel_on_person_role_changed), details_panel);
if (details_panel->entity_value_label == NULL || if (details_panel->entity_value_label == NULL ||
details_panel->entity_type_label == NULL || details_panel->entity_type_label == NULL ||
details_panel->entity_description_label == NULL || details_panel->entity_description_label == NULL ||
@ -812,6 +855,14 @@ void entity_details_panel_set_entity(
) )
); );
gboolean is_person = g_strcmp0(entity_record_get_type_identifier(
entity_record), "person") == 0;
gtk_widget_set_visible(details_panel->person_role_box, is_person);
details_panel->updating_person_role = TRUE;
gtk_drop_down_set_selected(details_panel->person_role_dropdown,
(guint) entity_record_get_person_role(entity_record));
details_panel->updating_person_role = FALSE;
entity_details_panel_update_add_relation_button( entity_details_panel_update_add_relation_button(
details_panel details_panel
); );
@ -1013,6 +1064,8 @@ void entity_details_panel_clear(
/* Le revealer fermé ne doit pas bloquer le canvas sous l'overlay. */ /* Le revealer fermé ne doit pas bloquer le canvas sous l'overlay. */
gtk_widget_set_can_target(details_panel->root_revealer, FALSE); gtk_widget_set_can_target(details_panel->root_revealer, FALSE);
} }
if (details_panel->person_role_box != NULL)
gtk_widget_set_visible(details_panel->person_role_box, FALSE);
} }
void entity_details_panel_set_close_callback( void entity_details_panel_set_close_callback(
@ -1055,6 +1108,15 @@ void entity_details_panel_set_add_relation_callback(
); );
} }
void entity_details_panel_set_person_role_callback(
EntityDetailsPanel *details_panel,
EntityDetailsPanelPersonRoleCallback callback, gpointer user_data)
{
if (details_panel == NULL) return;
details_panel->person_role_callback = callback;
details_panel->person_role_user_data = user_data;
}
gboolean entity_details_panel_is_open( gboolean entity_details_panel_is_open(
const EntityDetailsPanel *details_panel const EntityDetailsPanel *details_panel
) )

View file

@ -3304,6 +3304,8 @@ static void investigation_graph_view_draw_entity(
SocialPlatform social_platform = SocialPlatform social_platform =
SOCIAL_PLATFORM_NONE; SOCIAL_PLATFORM_NONE;
PersonRole person_role = PERSON_ROLE_UNCATEGORIZED;
double text_x = double text_x =
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING; x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING;
@ -3325,7 +3327,28 @@ static void investigation_graph_view_draw_entity(
INVESTIGATION_GRAPH_VIEW_NODE_RADIUS INVESTIGATION_GRAPH_VIEW_NODE_RADIUS
); );
if (selected) if (g_strcmp0(entity_record_get_type_identifier(entity_record),
"person") == 0)
{
person_role = entity_record_get_person_role(entity_record);
switch (person_role)
{
case PERSON_ROLE_ALLEGED_SCAMMER:
cairo_set_source_rgb(cairo_context, 0.55, 0.12, 0.14); break;
case PERSON_ROLE_VICTIM:
cairo_set_source_rgb(cairo_context, 0.12, 0.42, 0.22); break;
case PERSON_ROLE_WITNESS:
cairo_set_source_rgb(cairo_context, 0.12, 0.30, 0.56); break;
case PERSON_ROLE_SUSPECT:
cairo_set_source_rgb(cairo_context, 0.62, 0.32, 0.08); break;
case PERSON_ROLE_RELATED_PERSON:
cairo_set_source_rgb(cairo_context, 0.38, 0.20, 0.52); break;
case PERSON_ROLE_UNCATEGORIZED:
default:
cairo_set_source_rgb(cairo_context, 0.25, 0.27, 0.31); break;
}
}
else if (selected)
{ {
cairo_set_source_rgb( cairo_set_source_rgb(
cairo_context, cairo_context,
@ -3393,6 +3416,9 @@ static void investigation_graph_view_draw_entity(
social_platform = social_platform_from_entity_type(type_identifier); social_platform = social_platform_from_entity_type(type_identifier);
if (g_strcmp0(type_identifier, "person") == 0)
type_label = person_role_get_label(person_role);
if (social_platform != SOCIAL_PLATFORM_NONE) if (social_platform != SOCIAL_PLATFORM_NONE)
{ {
investigation_graph_view_draw_social_icon( investigation_graph_view_draw_social_icon(

View file

@ -124,6 +124,8 @@ struct Workspace
WorkspaceEditRelationCallback edit_relation_callback; WorkspaceEditRelationCallback edit_relation_callback;
gpointer edit_relation_user_data; gpointer edit_relation_user_data;
WorkspacePersonRoleCallback person_role_callback;
gpointer person_role_user_data;
WorkspaceOsintActionCallback WorkspaceOsintActionCallback
osint_action_callback; osint_action_callback;
@ -782,6 +784,16 @@ static void workspace_on_add_relation_requested(
); );
} }
/** @brief Relaie le changement de catégorie d'une personne. */
static void workspace_on_person_role_changed(const char *entity_identifier,
PersonRole role, gpointer user_data)
{
Workspace *workspace = user_data;
if (workspace != NULL && workspace->person_role_callback != NULL)
workspace->person_role_callback(entity_identifier, role,
workspace->person_role_user_data);
}
/** /**
* @brief Désélectionne le nœud lorsque le volet est fermé manuellement. * @brief Désélectionne le nœud lorsque le volet est fermé manuellement.
*/ */
@ -1654,6 +1666,10 @@ Workspace *workspace_new(void)
workspace workspace
); );
entity_details_panel_set_person_role_callback(
workspace->entity_details_panel, workspace_on_person_role_changed,
workspace);
gtk_overlay_set_child( gtk_overlay_set_child(
GTK_OVERLAY( GTK_OVERLAY(
workspace->graph_view_page workspace->graph_view_page
@ -2993,6 +3009,14 @@ void workspace_set_edit_relation_callback(Workspace *workspace,
workspace->edit_relation_user_data = user_data; workspace->edit_relation_user_data = user_data;
} }
void workspace_set_person_role_callback(Workspace *workspace,
WorkspacePersonRoleCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->person_role_callback = callback;
workspace->person_role_user_data = user_data;
}
void workspace_reset_graph_layout( void workspace_reset_graph_layout(
Workspace *workspace Workspace *workspace
) )
@ -3059,6 +3083,8 @@ void workspace_free(Workspace *workspace)
NULL, NULL,
NULL NULL
); );
entity_details_panel_set_person_role_callback(
workspace->entity_details_panel, NULL, NULL);
entity_details_panel_clear( entity_details_panel_clear(
workspace->entity_details_panel workspace->entity_details_panel

View file

@ -525,11 +525,12 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;" "FROM investigation;"
); );
assert(strcmp(schema_version, "4") == 0); assert(strcmp(schema_version, "5") == 0);
test_database_assert_table_exists(database, "osint_executions"); test_database_assert_table_exists(database, "osint_executions");
test_database_assert_table_exists(database, "osint_execution_entities"); test_database_assert_table_exists(database, "osint_execution_entities");
test_database_assert_table_exists(database, "osint_execution_relations"); test_database_assert_table_exists(database, "osint_execution_relations");
test_database_assert_table_exists(database, "comptes_sociaux"); test_database_assert_table_exists(database, "comptes_sociaux");
test_database_assert_table_exists(database, "person_roles");
assert(strcmp(application_name, "Labfy Investigation") == 0); assert(strcmp(application_name, "Labfy Investigation") == 0);
assert(created_at[0] != '\0'); assert(created_at[0] != '\0');
@ -988,7 +989,7 @@ static void test_database_migrate_v1_to_v2(void)
assert( assert(
strcmp( strcmp(
schema_version, schema_version,
"4" "5"
) == 0 ) == 0
); );

View file

@ -37,6 +37,13 @@ static void test_person_entity_create(void)
assert(record != NULL && error == NULL); assert(record != NULL && error == NULL);
assert(strcmp(entity_record_get_type_identifier(record), "person") == 0); assert(strcmp(entity_record_get_type_identifier(record), "person") == 0);
assert(entity_record_get_confidence(record) == 30); assert(entity_record_get_confidence(record) == 30);
assert(entity_record_get_person_role(record) == PERSON_ROLE_UNCATEGORIZED);
entity_record_free(record); record = NULL;
assert(person_entity_service_update_role(database, identifier,
PERSON_ROLE_VICTIM, &error));
record = entity_dao_find_by_identifier(dao, identifier, &error);
assert(record != NULL && error == NULL);
assert(entity_record_get_person_role(record) == PERSON_ROLE_VICTIM);
entity_record_free(record); entity_dao_free(dao); database_close(database); entity_record_free(record); entity_dao_free(dao); database_close(database);
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0); assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
g_free(identifier); g_free(path); g_free(directory); g_free(identifier); g_free(path); g_free(directory);