feat(graph): display social platform icons

This commit is contained in:
grayTerminal-sh 2026-07-22 15:20:24 +02:00
parent e91c5f5ee0
commit 800dee9b76
7 changed files with 322 additions and 13 deletions

View file

@ -122,6 +122,7 @@ TEST_OSINT_EXECUTION_DAO := tests/test_osint_execution_dao
TEST_OSINT_EXECUTION_INTEGRITY := tests/test_osint_execution_integrity TEST_OSINT_EXECUTION_INTEGRITY := tests/test_osint_execution_integrity
TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
TEST_SOCIAL_PLATFORM := tests/test_social_platform
all: $(TARGET) all: $(TARGET)
@ -623,6 +624,11 @@ $(TEST_SOCIAL_ACCOUNT_SERVICE): \
src/database/error.c src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_SOCIAL_PLATFORM): \
tests/test_social_platform.c \
src/models/social_platform.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
tests/test_investigation_graph_load_task.c \ tests/test_investigation_graph_load_task.c \
src/core/investigation_graph_load_task.c \ src/core/investigation_graph_load_task.c \
@ -699,7 +705,8 @@ test: \
$(TEST_OSINT_EXECUTION_DAO) \ $(TEST_OSINT_EXECUTION_DAO) \
$(TEST_OSINT_EXECUTION_INTEGRITY) \ $(TEST_OSINT_EXECUTION_INTEGRITY) \
$(TEST_EVIDENCE_RECLASSIFICATION) \ $(TEST_EVIDENCE_RECLASSIFICATION) \
$(TEST_SOCIAL_ACCOUNT_SERVICE) $(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -756,6 +763,7 @@ test: \
@$(TEST_OSINT_EXECUTION_INTEGRITY) @$(TEST_OSINT_EXECUTION_INTEGRITY)
@$(TEST_EVIDENCE_RECLASSIFICATION) @$(TEST_EVIDENCE_RECLASSIFICATION)
@$(TEST_SOCIAL_ACCOUNT_SERVICE) @$(TEST_SOCIAL_ACCOUNT_SERVICE)
@$(TEST_SOCIAL_PLATFORM)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -818,7 +826,8 @@ clean:
$(TEST_OSINT_EXECUTION_DAO) \ $(TEST_OSINT_EXECUTION_DAO) \
$(TEST_OSINT_EXECUTION_INTEGRITY) \ $(TEST_OSINT_EXECUTION_INTEGRITY) \
$(TEST_EVIDENCE_RECLASSIFICATION) \ $(TEST_EVIDENCE_RECLASSIFICATION) \
$(TEST_SOCIAL_ACCOUNT_SERVICE) $(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM)
-include $(DEP) -include $(DEP)

View file

@ -161,6 +161,8 @@ Le socle actuel comprend notamment :
- comptes sociaux structurés en SQLite V4 (TikTok, Instagram, Facebook, X, - comptes sociaux structurés en SQLite V4 (TikTok, Instagram, Facebook, X,
Telegram ou autre), avec URL, pseudonyme, identifiant stable facultatif, Telegram ou autre), avec URL, pseudonyme, identifiant stable facultatif,
première observation, état, notes et rattachement à une preuve ; première observation, état, notes et rattachement à une preuve ;
- pictogrammes vectoriels des plateformes sociales dans les nœuds du graphe,
conservant leur lisibilité pendant le zoom ;
- historique OSINT contextuel en lecture seule avec détail des exécutions, - historique OSINT contextuel en lecture seule avec détail des exécutions,
sorties standard et d'erreur, et objets créés ou réutilisés ; sorties standard et d'erreur, et objets créés ou réutilisés ;
- vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans - vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans

View file

@ -0,0 +1,45 @@
/******************************************************************************
* @file social_platform.h
* @brief Identification visuelle des plateformes sociales du graphe.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_SOCIAL_PLATFORM_H
#define LABFY_INVESTIGATION_SOCIAL_PLATFORM_H
#include <glib.h>
G_BEGIN_DECLS
/** @brief Plateformes possédant un pictogramme dans le graphe. */
typedef enum
{
SOCIAL_PLATFORM_NONE,
SOCIAL_PLATFORM_TIKTOK,
SOCIAL_PLATFORM_INSTAGRAM,
SOCIAL_PLATFORM_FACEBOOK,
SOCIAL_PLATFORM_X,
SOCIAL_PLATFORM_TELEGRAM,
SOCIAL_PLATFORM_OTHER
} SocialPlatform;
/**
* @brief Déduit la plateforme depuis le code du type d'entité.
* @param entity_type_identifier Code métier du type d'entité.
* @return Plateforme correspondante, ou SOCIAL_PLATFORM_NONE.
*/
SocialPlatform social_platform_from_entity_type(
const char *entity_type_identifier
);
/**
* @brief Retourne le libellé français affichable d'une plateforme.
* @param platform Plateforme à décrire.
* @return Chaîne statique, ou NULL pour SOCIAL_PLATFORM_NONE.
*/
const char *social_platform_get_label(
SocialPlatform platform
);
G_END_DECLS
#endif

View file

@ -0,0 +1,42 @@
/******************************************************************************
* @file social_platform.c
* @brief Identification visuelle des plateformes sociales du graphe.
******************************************************************************/
#include "models/social_platform.h"
SocialPlatform social_platform_from_entity_type(
const char *entity_type_identifier
)
{
if (g_strcmp0(entity_type_identifier, "tiktok_account") == 0)
return SOCIAL_PLATFORM_TIKTOK;
if (g_strcmp0(entity_type_identifier, "instagram_account") == 0)
return SOCIAL_PLATFORM_INSTAGRAM;
if (g_strcmp0(entity_type_identifier, "facebook_account") == 0)
return SOCIAL_PLATFORM_FACEBOOK;
if (g_strcmp0(entity_type_identifier, "x_account") == 0)
return SOCIAL_PLATFORM_X;
if (g_strcmp0(entity_type_identifier, "telegram_account") == 0)
return SOCIAL_PLATFORM_TELEGRAM;
if (g_strcmp0(entity_type_identifier, "social_account") == 0)
return SOCIAL_PLATFORM_OTHER;
return SOCIAL_PLATFORM_NONE;
}
const char *social_platform_get_label(
SocialPlatform platform
)
{
switch (platform)
{
case SOCIAL_PLATFORM_TIKTOK: return "TikTok";
case SOCIAL_PLATFORM_INSTAGRAM: return "Instagram";
case SOCIAL_PLATFORM_FACEBOOK: return "Facebook";
case SOCIAL_PLATFORM_X: return "X";
case SOCIAL_PLATFORM_TELEGRAM: return "Telegram";
case SOCIAL_PLATFORM_OTHER: return "Réseau social";
case SOCIAL_PLATFORM_NONE: return NULL;
}
return NULL;
}

View file

@ -110,17 +110,34 @@ static void create_social_account_dialog_on_create(GtkButton *button, gpointer u
const char *username = gtk_editable_get_text(GTK_EDITABLE(state->username)); const char *username = gtk_editable_get_text(GTK_EDITABLE(state->username));
const char *observed_at = gtk_editable_get_text(GTK_EDITABLE(state->observed_at)); const char *observed_at = gtk_editable_get_text(GTK_EDITABLE(state->observed_at));
(void) button; (void) button;
date = g_date_time_new_from_iso8601(observed_at, NULL);
if (platform >= G_N_ELEMENTS(platform_codes) || if (platform >= G_N_ELEMENTS(platform_codes) ||
account_state >= G_N_ELEMENTS(state_codes) || account_state >= G_N_ELEMENTS(state_codes))
url == NULL || {
(!g_str_has_prefix(url, "https://") && !g_str_has_prefix(url, "http://")) || gtk_label_set_text(state->error, "Sélectionnez une plateforme et un état.");
username == NULL || username[0] == '\0' || date == NULL) gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (url == NULL ||
(!g_str_has_prefix(url, "https://") && !g_str_has_prefix(url, "http://")))
{ {
gtk_label_set_text(state->error, gtk_label_set_text(state->error,
"Renseignez une URL http(s), un pseudonyme et une date ISO 8601 valide."); "L'URL du profil doit commencer par https:// ou http://.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (username == NULL || username[0] == '\0')
{
gtk_label_set_text(state->error,
"Le pseudonyme affiché est obligatoire (par exemple @compte).");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
date = g_date_time_new_from_iso8601(observed_at, NULL);
if (date == NULL)
{
gtk_label_set_text(state->error,
"La date doit respecter le format UTC AAAA-MM-JJTHH:MM:SSZ.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE); gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
g_clear_pointer(&date, g_date_time_unref);
return; return;
} }
notes = create_social_account_dialog_notes(state); notes = create_social_account_dialog_notes(state);
@ -217,7 +234,8 @@ gboolean create_social_account_dialog_present(
} }
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new( state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(evidence_labels), NULL)); G_LIST_MODEL(evidence_labels), NULL));
g_object_unref(evidence_labels); /* gtk_drop_down_new() prend possession de la référence du modèle. */
evidence_labels = NULL;
state->notes = GTK_TEXT_VIEW(gtk_text_view_new()); state->notes = GTK_TEXT_VIEW(gtk_text_view_new());
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR); gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 100); gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 100);

View file

@ -9,6 +9,7 @@
#include "models/investigation_graph_layout.h" #include "models/investigation_graph_layout.h"
#include "models/investigation_graph_model.h" #include "models/investigation_graph_model.h"
#include "models/relation_record.h" #include "models/relation_record.h"
#include "models/social_platform.h"
#include <glib.h> #include <glib.h>
#include <pango/pangocairo.h> #include <pango/pangocairo.h>
@ -23,6 +24,8 @@
#define INVESTIGATION_GRAPH_VIEW_NODE_RADIUS 12.0 #define INVESTIGATION_GRAPH_VIEW_NODE_RADIUS 12.0
#define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_RADIUS 10.0 #define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_RADIUS 10.0
#define INVESTIGATION_GRAPH_VIEW_NODE_PADDING 12 #define INVESTIGATION_GRAPH_VIEW_NODE_PADDING 12
#define INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_SIZE 38.0
#define INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_GAP 10.0
#define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_GAP 72.0 #define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_GAP 72.0
#define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_CLEARANCE 16.0 #define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_CLEARANCE 16.0
#define INVESTIGATION_GRAPH_VIEW_ARROW_LENGTH 9.0 #define INVESTIGATION_GRAPH_VIEW_ARROW_LENGTH 9.0
@ -3127,6 +3130,130 @@ static void investigation_graph_view_draw_relations(
} }
} }
/** @brief Configure la couleur de fond propre à une plateforme sociale. */
static void investigation_graph_view_set_social_icon_color(
cairo_t *cairo_context,
SocialPlatform platform
)
{
switch (platform)
{
case SOCIAL_PLATFORM_TIKTOK:
cairo_set_source_rgb(cairo_context, 0.06, 0.06, 0.08);
break;
case SOCIAL_PLATFORM_INSTAGRAM:
cairo_set_source_rgb(cairo_context, 0.76, 0.18, 0.48);
break;
case SOCIAL_PLATFORM_FACEBOOK:
cairo_set_source_rgb(cairo_context, 0.10, 0.36, 0.72);
break;
case SOCIAL_PLATFORM_X:
cairo_set_source_rgb(cairo_context, 0.05, 0.06, 0.08);
break;
case SOCIAL_PLATFORM_TELEGRAM:
cairo_set_source_rgb(cairo_context, 0.12, 0.60, 0.84);
break;
case SOCIAL_PLATFORM_OTHER:
case SOCIAL_PLATFORM_NONE:
cairo_set_source_rgb(cairo_context, 0.34, 0.39, 0.50);
break;
}
}
/** @brief Dessine le symbole blanc propre à une plateforme sociale. */
static void investigation_graph_view_draw_social_symbol(
cairo_t *cairo_context,
SocialPlatform platform,
double center_x,
double center_y
)
{
cairo_set_source_rgb(cairo_context, 1.0, 1.0, 1.0);
cairo_set_line_width(cairo_context, 2.3);
cairo_set_line_cap(cairo_context, CAIRO_LINE_CAP_ROUND);
cairo_set_line_join(cairo_context, CAIRO_LINE_JOIN_ROUND);
switch (platform)
{
case SOCIAL_PLATFORM_TIKTOK:
cairo_arc(cairo_context, center_x - 4.0, center_y + 6.0,
4.2, 0.0, 2.0 * G_PI);
cairo_fill(cairo_context);
cairo_move_to(cairo_context, center_x, center_y + 6.0);
cairo_line_to(cairo_context, center_x, center_y - 9.0);
cairo_line_to(cairo_context, center_x + 4.0, center_y - 6.0);
cairo_line_to(cairo_context, center_x + 8.0, center_y - 5.0);
cairo_stroke(cairo_context);
break;
case SOCIAL_PLATFORM_INSTAGRAM:
investigation_graph_view_rounded_rectangle(cairo_context,
center_x - 10.0, center_y - 10.0, 20.0, 20.0, 5.0);
cairo_stroke(cairo_context);
cairo_arc(cairo_context, center_x, center_y, 4.7,
0.0, 2.0 * G_PI);
cairo_stroke(cairo_context);
cairo_arc(cairo_context, center_x + 6.0, center_y - 6.0,
1.4, 0.0, 2.0 * G_PI);
cairo_fill(cairo_context);
break;
case SOCIAL_PLATFORM_FACEBOOK:
investigation_graph_view_draw_text(cairo_context, "f",
center_x - 5.0, center_y - 14.0, 14,
"Sans Bold 21", 1.0, 1.0, 1.0);
break;
case SOCIAL_PLATFORM_X:
cairo_move_to(cairo_context, center_x - 8.0, center_y - 9.0);
cairo_line_to(cairo_context, center_x + 8.0, center_y + 9.0);
cairo_move_to(cairo_context, center_x + 8.0, center_y - 9.0);
cairo_line_to(cairo_context, center_x - 8.0, center_y + 9.0);
cairo_stroke(cairo_context);
break;
case SOCIAL_PLATFORM_TELEGRAM:
cairo_move_to(cairo_context, center_x - 11.0, center_y - 1.0);
cairo_line_to(cairo_context, center_x + 11.0, center_y - 9.0);
cairo_line_to(cairo_context, center_x + 5.0, center_y + 10.0);
cairo_line_to(cairo_context, center_x, center_y + 3.0);
cairo_line_to(cairo_context, center_x - 5.0, center_y + 7.0);
cairo_close_path(cairo_context);
cairo_fill(cairo_context);
break;
case SOCIAL_PLATFORM_OTHER:
cairo_arc(cairo_context, center_x, center_y - 6.0, 4.0,
0.0, 2.0 * G_PI);
cairo_arc(cairo_context, center_x - 8.0, center_y + 7.0, 3.0,
0.0, 2.0 * G_PI);
cairo_arc(cairo_context, center_x + 8.0, center_y + 7.0, 3.0,
0.0, 2.0 * G_PI);
cairo_fill(cairo_context);
cairo_move_to(cairo_context, center_x, center_y - 2.0);
cairo_line_to(cairo_context, center_x - 7.0, center_y + 4.0);
cairo_move_to(cairo_context, center_x, center_y - 2.0);
cairo_line_to(cairo_context, center_x + 7.0, center_y + 4.0);
cairo_stroke(cairo_context);
break;
case SOCIAL_PLATFORM_NONE:
break;
}
}
/** @brief Dessine le badge vectoriel d'un compte social. */
static void investigation_graph_view_draw_social_icon(
cairo_t *cairo_context,
SocialPlatform platform,
double x,
double y
)
{
double center_x = x + (INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_SIZE / 2.0);
double center_y = y + (INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_SIZE / 2.0);
investigation_graph_view_set_social_icon_color(cairo_context, platform);
cairo_arc(cairo_context, center_x, center_y,
INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_SIZE / 2.0,
0.0, 2.0 * G_PI);
cairo_fill(cairo_context);
investigation_graph_view_draw_social_symbol(
cairo_context, platform, center_x, center_y);
}
/** /**
* @brief Dessine un nœud d'entité. * @brief Dessine un nœud d'entité.
*/ */
@ -3144,6 +3271,15 @@ static void investigation_graph_view_draw_entity(
const char *type_identifier = const char *type_identifier =
NULL; NULL;
const char *type_label =
NULL;
SocialPlatform social_platform =
SOCIAL_PLATFORM_NONE;
double text_x =
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING;
int text_width = int text_width =
(int) INVESTIGATION_GRAPH_VIEW_NODE_WIDTH - (int) INVESTIGATION_GRAPH_VIEW_NODE_WIDTH -
(2 * INVESTIGATION_GRAPH_VIEW_NODE_PADDING); (2 * INVESTIGATION_GRAPH_VIEW_NODE_PADDING);
@ -3228,10 +3364,27 @@ static void investigation_graph_view_draw_entity(
entity_record entity_record
); );
social_platform = social_platform_from_entity_type(type_identifier);
if (social_platform != SOCIAL_PLATFORM_NONE)
{
investigation_graph_view_draw_social_icon(
cairo_context,
social_platform,
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING,
y + 20.0
);
text_x += INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_SIZE +
INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_GAP;
text_width -= (int) (INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_SIZE +
INVESTIGATION_GRAPH_VIEW_SOCIAL_ICON_GAP);
type_label = social_platform_get_label(social_platform);
}
investigation_graph_view_draw_text( investigation_graph_view_draw_text(
cairo_context, cairo_context,
title, title,
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING, text_x,
y + 13.0, y + 13.0,
text_width, text_width,
"Sans Bold 11", "Sans Bold 11",
@ -3242,11 +3395,13 @@ static void investigation_graph_view_draw_entity(
investigation_graph_view_draw_text( investigation_graph_view_draw_text(
cairo_context, cairo_context,
type_identifier != NULL && type_label != NULL
? type_label
: type_identifier != NULL &&
type_identifier[0] != '\0' type_identifier[0] != '\0'
? type_identifier ? type_identifier
: "(type inconnu)", : "(type inconnu)",
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING, text_x,
y + 44.0, y + 44.0,
text_width, text_width,
"Sans 9", "Sans 9",

View file

@ -0,0 +1,38 @@
/******************************************************************************
* @file test_social_platform.c
* @brief Tests de la correspondance visuelle des plateformes sociales.
******************************************************************************/
#include "models/social_platform.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
/** @brief Vérifie tous les types sociaux persistés et le cas générique. */
static void test_social_platform_mapping(void)
{
assert(social_platform_from_entity_type("tiktok_account") ==
SOCIAL_PLATFORM_TIKTOK);
assert(social_platform_from_entity_type("instagram_account") ==
SOCIAL_PLATFORM_INSTAGRAM);
assert(social_platform_from_entity_type("facebook_account") ==
SOCIAL_PLATFORM_FACEBOOK);
assert(social_platform_from_entity_type("x_account") == SOCIAL_PLATFORM_X);
assert(social_platform_from_entity_type("telegram_account") ==
SOCIAL_PLATFORM_TELEGRAM);
assert(social_platform_from_entity_type("social_account") ==
SOCIAL_PLATFORM_OTHER);
assert(social_platform_from_entity_type("person") == SOCIAL_PLATFORM_NONE);
assert(social_platform_from_entity_type(NULL) == SOCIAL_PLATFORM_NONE);
assert(strcmp(social_platform_get_label(SOCIAL_PLATFORM_INSTAGRAM),
"Instagram") == 0);
assert(social_platform_get_label(SOCIAL_PLATFORM_NONE) == NULL);
}
int main(void)
{
test_social_platform_mapping();
puts("SocialPlatform : tous les tests sont valides.");
return 0;
}