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_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
TEST_SOCIAL_PLATFORM := tests/test_social_platform
all: $(TARGET)
@ -623,6 +624,11 @@ $(TEST_SOCIAL_ACCOUNT_SERVICE): \
src/database/error.c
$(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): \
tests/test_investigation_graph_load_task.c \
src/core/investigation_graph_load_task.c \
@ -699,7 +705,8 @@ test: \
$(TEST_OSINT_EXECUTION_DAO) \
$(TEST_OSINT_EXECUTION_INTEGRITY) \
$(TEST_EVIDENCE_RECLASSIFICATION) \
$(TEST_SOCIAL_ACCOUNT_SERVICE)
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -756,6 +763,7 @@ test: \
@$(TEST_OSINT_EXECUTION_INTEGRITY)
@$(TEST_EVIDENCE_RECLASSIFICATION)
@$(TEST_SOCIAL_ACCOUNT_SERVICE)
@$(TEST_SOCIAL_PLATFORM)
@echo "Tous les tests sont valides."
%.o: %.c
@ -818,7 +826,8 @@ clean:
$(TEST_OSINT_EXECUTION_DAO) \
$(TEST_OSINT_EXECUTION_INTEGRITY) \
$(TEST_EVIDENCE_RECLASSIFICATION) \
$(TEST_SOCIAL_ACCOUNT_SERVICE)
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM)
-include $(DEP)

View file

@ -161,6 +161,8 @@ Le socle actuel comprend notamment :
- comptes sociaux structurés en SQLite V4 (TikTok, Instagram, Facebook, X,
Telegram ou autre), avec URL, pseudonyme, identifiant stable facultatif,
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,
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

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 *observed_at = gtk_editable_get_text(GTK_EDITABLE(state->observed_at));
(void) button;
date = g_date_time_new_from_iso8601(observed_at, NULL);
if (platform >= G_N_ELEMENTS(platform_codes) ||
account_state >= G_N_ELEMENTS(state_codes) ||
url == NULL ||
(!g_str_has_prefix(url, "https://") && !g_str_has_prefix(url, "http://")) ||
username == NULL || username[0] == '\0' || date == NULL)
account_state >= G_N_ELEMENTS(state_codes))
{
gtk_label_set_text(state->error, "Sélectionnez une plateforme et un état.");
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,
"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);
g_clear_pointer(&date, g_date_time_unref);
return;
}
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(
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());
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
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_model.h"
#include "models/relation_record.h"
#include "models/social_platform.h"
#include <glib.h>
#include <pango/pangocairo.h>
@ -23,6 +24,8 @@
#define INVESTIGATION_GRAPH_VIEW_NODE_RADIUS 12.0
#define INVESTIGATION_GRAPH_VIEW_RELATION_NODE_RADIUS 10.0
#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_CLEARANCE 16.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é.
*/
@ -3144,6 +3271,15 @@ static void investigation_graph_view_draw_entity(
const char *type_identifier =
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) INVESTIGATION_GRAPH_VIEW_NODE_WIDTH -
(2 * INVESTIGATION_GRAPH_VIEW_NODE_PADDING);
@ -3228,10 +3364,27 @@ static void investigation_graph_view_draw_entity(
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(
cairo_context,
title,
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING,
text_x,
y + 13.0,
text_width,
"Sans Bold 11",
@ -3242,11 +3395,13 @@ static void investigation_graph_view_draw_entity(
investigation_graph_view_draw_text(
cairo_context,
type_identifier != NULL &&
type_label != NULL
? type_label
: type_identifier != NULL &&
type_identifier[0] != '\0'
? type_identifier
: "(type inconnu)",
x + INVESTIGATION_GRAPH_VIEW_NODE_PADDING,
text_x,
y + 44.0,
text_width,
"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;
}