diff --git a/Makefile b/Makefile index a274144..48c2ecf 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 37a18e8..90c41c1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/include/models/social_platform.h b/include/models/social_platform.h new file mode 100644 index 0000000..026425d --- /dev/null +++ b/include/models/social_platform.h @@ -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 + +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 diff --git a/src/models/social_platform.c b/src/models/social_platform.c new file mode 100644 index 0000000..56eecc4 --- /dev/null +++ b/src/models/social_platform.c @@ -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; +} diff --git a/src/views/create_social_account_dialog.c b/src/views/create_social_account_dialog.c index ba05035..efd9f03 100644 --- a/src/views/create_social_account_dialog.c +++ b/src/views/create_social_account_dialog.c @@ -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); diff --git a/src/widgets/investigation_graph_view.c b/src/widgets/investigation_graph_view.c index bebc64f..3432248 100644 --- a/src/widgets/investigation_graph_view.c +++ b/src/widgets/investigation_graph_view.c @@ -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 #include @@ -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", diff --git a/tests/test_social_platform.c b/tests/test_social_platform.c new file mode 100644 index 0000000..c8b94d9 --- /dev/null +++ b/tests/test_social_platform.c @@ -0,0 +1,38 @@ +/****************************************************************************** + * @file test_social_platform.c + * @brief Tests de la correspondance visuelle des plateformes sociales. + ******************************************************************************/ + +#include "models/social_platform.h" + +#include +#include +#include + +/** @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; +}