feat(entities): add social media accounts
This commit is contained in:
parent
d876590529
commit
e91c5f5ee0
18 changed files with 934 additions and 9 deletions
21
Makefile
21
Makefile
|
|
@ -121,6 +121,7 @@ TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration
|
|||
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
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
|
|
@ -609,6 +610,19 @@ $(TEST_EVIDENCE_RECLASSIFICATION): \
|
|||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_SOCIAL_ACCOUNT_SERVICE): \
|
||||
tests/test_social_account_service.c \
|
||||
src/core/social_account_service.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/dao/evidence_entity_dao.c \
|
||||
src/models/entity_record.c \
|
||||
src/database/database.c \
|
||||
src/database/schema.c \
|
||||
src/database/statement.c \
|
||||
src/database/transaction.c \
|
||||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
|
||||
tests/test_investigation_graph_load_task.c \
|
||||
src/core/investigation_graph_load_task.c \
|
||||
|
|
@ -684,7 +698,8 @@ test: \
|
|||
$(TEST_OSINT_DNS_INTEGRATION) \
|
||||
$(TEST_OSINT_EXECUTION_DAO) \
|
||||
$(TEST_OSINT_EXECUTION_INTEGRITY) \
|
||||
$(TEST_EVIDENCE_RECLASSIFICATION)
|
||||
$(TEST_EVIDENCE_RECLASSIFICATION) \
|
||||
$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||
@echo "Exécution des tests..."
|
||||
@./$(TEST_NODE)
|
||||
@./$(TEST_TREE_MODEL)
|
||||
|
|
@ -740,6 +755,7 @@ test: \
|
|||
@$(TEST_OSINT_EXECUTION_DAO)
|
||||
@$(TEST_OSINT_EXECUTION_INTEGRITY)
|
||||
@$(TEST_EVIDENCE_RECLASSIFICATION)
|
||||
@$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||
@echo "Tous les tests sont valides."
|
||||
|
||||
%.o: %.c
|
||||
|
|
@ -801,7 +817,8 @@ clean:
|
|||
$(TEST_OSINT_DNS_INTEGRATION) \
|
||||
$(TEST_OSINT_EXECUTION_DAO) \
|
||||
$(TEST_OSINT_EXECUTION_INTEGRITY) \
|
||||
$(TEST_EVIDENCE_RECLASSIFICATION)
|
||||
$(TEST_EVIDENCE_RECLASSIFICATION) \
|
||||
$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||
|
||||
-include $(DEP)
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ Le socle actuel comprend notamment :
|
|||
`uses_name_server` depuis l'entité interrogée ;
|
||||
- provenance OSINT SQLite V3 conservant les arguments, sorties brutes,
|
||||
empreinte SHA-256 et liaisons vers les entités et relations intégrées ;
|
||||
- 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 ;
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/******************************************************************************
|
||||
* Labfy Investigation
|
||||
*
|
||||
* Extensions idempotentes du schéma SQLite courant V3
|
||||
* Extensions idempotentes du schéma SQLite courant V4
|
||||
******************************************************************************/
|
||||
|
||||
/*
|
||||
|
|
|
|||
39
database/schema_v4.sql
Normal file
39
database/schema_v4.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/******************************************************************************
|
||||
* Labfy Investigation
|
||||
*
|
||||
* Migration du schéma SQLite V3 vers V4 : comptes de réseaux sociaux
|
||||
******************************************************************************/
|
||||
|
||||
INSERT INTO types_entite (code, label, description) VALUES
|
||||
('tiktok_account', 'Compte TikTok', NULL),
|
||||
('x_account', 'Compte X', NULL),
|
||||
('telegram_account', 'Compte Telegram', NULL),
|
||||
('social_account', 'Autre compte social', NULL);
|
||||
|
||||
CREATE TABLE comptes_sociaux
|
||||
(
|
||||
entite_id TEXT PRIMARY KEY,
|
||||
plateforme TEXT NOT NULL,
|
||||
url_profil TEXT NOT NULL,
|
||||
pseudonyme TEXT NOT NULL,
|
||||
identifiant_plateforme TEXT,
|
||||
premiere_observation TEXT NOT NULL,
|
||||
etat_compte TEXT NOT NULL DEFAULT 'unknown',
|
||||
notes TEXT,
|
||||
|
||||
FOREIGN KEY (entite_id)
|
||||
REFERENCES entites(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
|
||||
UNIQUE (plateforme, url_profil),
|
||||
|
||||
CHECK (plateforme IN ('tiktok', 'instagram', 'facebook', 'x', 'telegram', 'other')),
|
||||
CHECK (length(trim(url_profil)) > 0),
|
||||
CHECK (length(trim(pseudonyme)) > 0),
|
||||
CHECK (length(premiere_observation) = 20),
|
||||
CHECK (etat_compte IN ('active', 'private', 'suspended', 'deleted', 'unknown'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_comptes_sociaux_pseudonyme
|
||||
ON comptes_sociaux(plateforme, pseudonyme);
|
||||
|
|
@ -1148,6 +1148,19 @@ lors de l'enregistrement. Le résultat indique si les sorties sont intactes,
|
|||
altérées ou impossibles à vérifier. Ce contrôle est strictement en lecture
|
||||
seule : aucune sortie ni empreinte persistée n'est corrigée automatiquement.
|
||||
|
||||
### Comptes sociaux structurés — schéma V4
|
||||
|
||||
La table `comptes_sociaux` complète une ligne de `entites` sans dupliquer le
|
||||
nœud affiché dans le graphe. Elle conserve la plateforme, l'URL de profil, le
|
||||
pseudonyme affiché, l'identifiant stable facultatif, la première observation,
|
||||
l'état observé et les notes factuelles. La contrainte unique sur
|
||||
`(plateforme, url_profil)` évite les doublons.
|
||||
|
||||
Une capture, une vidéo ou un courriel déjà importé peut être rattaché au
|
||||
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
|
||||
échoue.
|
||||
|
||||
---
|
||||
|
||||
# 6. Tables de liaison
|
||||
|
|
|
|||
50
include/core/social_account_service.h
Normal file
50
include/core/social_account_service.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/******************************************************************************
|
||||
* @file social_account_service.h
|
||||
* @brief Création transactionnelle d'entités de comptes sociaux.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_SOCIAL_ACCOUNT_SERVICE_H
|
||||
#define LABFY_INVESTIGATION_SOCIAL_ACCOUNT_SERVICE_H
|
||||
|
||||
#include "database/database.h"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/** @brief Données nécessaires à l'enregistrement d'un compte social. */
|
||||
typedef struct
|
||||
{
|
||||
const char *platform;
|
||||
const char *profile_url;
|
||||
const char *username;
|
||||
const char *platform_identifier;
|
||||
const char *first_observed_at;
|
||||
const char *account_state;
|
||||
const char *notes;
|
||||
const char *evidence_identifier;
|
||||
} SocialAccountInput;
|
||||
|
||||
/**
|
||||
* @brief Valide et enregistre un compte social et sa preuve facultative.
|
||||
*
|
||||
* L'entité, sa fiche spécialisée et son éventuelle association à une preuve
|
||||
* sont créées dans une même transaction. L'identifiant retourné appartient à
|
||||
* l'appelant et doit être libéré avec g_free().
|
||||
*
|
||||
* @param database Connexion ouverte empruntée.
|
||||
* @param input Données du compte à créer.
|
||||
* @param out_entity_identifier Destination facultative de l'UUID créé.
|
||||
* @param error Emplacement facultatif recevant une erreur.
|
||||
* @return TRUE lorsque toutes les écritures sont validées.
|
||||
*/
|
||||
gboolean social_account_service_create(
|
||||
Database *database,
|
||||
const SocialAccountInput *input,
|
||||
char **out_entity_identifier,
|
||||
GError **error
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -68,6 +68,19 @@ bool schema_install_v3(
|
|||
Database *database
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Installe la migration des comptes sociaux du schéma V4.
|
||||
*
|
||||
* La connexion doit être valide et une transaction doit déjà être active.
|
||||
* La fonction ne réalise ni COMMIT ni ROLLBACK.
|
||||
*
|
||||
* @param database Connexion Database ouverte.
|
||||
* @return true si la migration V4 a été appliquée, sinon false.
|
||||
*/
|
||||
bool schema_install_v4(
|
||||
Database *database
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||
*
|
||||
|
|
|
|||
67
include/views/create_social_account_dialog.h
Normal file
67
include/views/create_social_account_dialog.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/******************************************************************************
|
||||
* @file create_social_account_dialog.h
|
||||
* @brief Dialogue GTK de création d'un compte social observé.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_CREATE_SOCIAL_ACCOUNT_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_CREATE_SOCIAL_ACCOUNT_DIALOG_H
|
||||
|
||||
#include "models/evidence_record.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/** @brief Résultat opaque du dialogue de compte social. */
|
||||
typedef struct CreateSocialAccountDialogResult CreateSocialAccountDialogResult;
|
||||
|
||||
/** @brief Callback appelé après validation ou annulation. */
|
||||
typedef void (*CreateSocialAccountDialogCallback)(
|
||||
CreateSocialAccountDialogResult *result,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Présente le formulaire de création d'un compte social.
|
||||
*
|
||||
* Le tableau de preuves est uniquement emprunté pendant cet appel ; les
|
||||
* informations nécessaires au formulaire sont copiées.
|
||||
*
|
||||
* @param parent Fenêtre parente.
|
||||
* @param evidence_records Tableau facultatif de EvidenceRecord.
|
||||
* @param callback Callback de fin.
|
||||
* @param user_data Données privées du callback.
|
||||
* @return TRUE si le dialogue a été présenté.
|
||||
*/
|
||||
gboolean create_social_account_dialog_present(
|
||||
GtkWindow *parent,
|
||||
const GPtrArray *evidence_records,
|
||||
CreateSocialAccountDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Libère un résultat, y compris toutes ses chaînes. */
|
||||
void create_social_account_dialog_result_free(
|
||||
CreateSocialAccountDialogResult *result
|
||||
);
|
||||
|
||||
/** @brief Retourne le code de la plateforme sélectionnée. */
|
||||
const char *create_social_account_dialog_result_get_platform(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'URL complète du profil. */
|
||||
const char *create_social_account_dialog_result_get_profile_url(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne le pseudonyme affiché. */
|
||||
const char *create_social_account_dialog_result_get_username(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'identifiant stable facultatif de la plateforme. */
|
||||
const char *create_social_account_dialog_result_get_platform_identifier(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne la date UTC de première observation. */
|
||||
const char *create_social_account_dialog_result_get_first_observed_at(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'état observé du compte. */
|
||||
const char *create_social_account_dialog_result_get_account_state(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne les notes factuelles facultatives. */
|
||||
const char *create_social_account_dialog_result_get_notes(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'UUID de la preuve associée, ou NULL. */
|
||||
const char *create_social_account_dialog_result_get_evidence_identifier(const CreateSocialAccountDialogResult *result);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -50,6 +50,11 @@ typedef void (*MainWindowImportEvidenceCallback)(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour ajouter un compte de réseau social. */
|
||||
typedef void (*MainWindowAddSocialAccountCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur revient au graphe.
|
||||
*
|
||||
|
|
@ -531,6 +536,18 @@ void main_window_set_import_evidence_callback(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de l'action d'ajout d'un compte social.
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_add_social_account_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowAddSocialAccountCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton « Revenir au graphe ».
|
||||
*
|
||||
|
|
@ -557,6 +574,16 @@ void main_window_set_import_evidence_enabled(
|
|||
gboolean enabled
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Active ou désactive l'ajout d'un compte social.
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param enabled TRUE si une enquête est ouverte.
|
||||
*/
|
||||
void main_window_set_add_social_account_enabled(
|
||||
MainWindow *main_window,
|
||||
gboolean enabled
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère les ressources de la fenêtre.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@
|
|||
#include "core/evidence_integrity_task.h"
|
||||
#include "core/evidence_integrity_verifier.h"
|
||||
#include "core/relation_service.h"
|
||||
#include "core/social_account_service.h"
|
||||
#include "database/database.h"
|
||||
#include "views/create_social_account_dialog.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <errno.h>
|
||||
|
|
@ -2267,6 +2269,11 @@ static gboolean application_install_session(
|
|||
TRUE
|
||||
);
|
||||
|
||||
main_window_set_add_social_account_enabled(
|
||||
application->main_window,
|
||||
TRUE
|
||||
);
|
||||
|
||||
application_start_graph_loading(
|
||||
application,
|
||||
database_path
|
||||
|
|
@ -4091,6 +4098,80 @@ static void application_on_import_evidence_requested(
|
|||
);
|
||||
}
|
||||
|
||||
/** @brief Persiste le résultat validé du dialogue de compte social. */
|
||||
static void application_on_social_account_completed(
|
||||
CreateSocialAccountDialogResult *result, gpointer user_data)
|
||||
{
|
||||
Application *application = user_data;
|
||||
SocialAccountInput input = {0};
|
||||
const InvestigationProject *project = NULL;
|
||||
Database *database = NULL;
|
||||
GError *error = NULL;
|
||||
char *identifier = NULL;
|
||||
if (result == NULL || application == NULL || application->session == NULL)
|
||||
{
|
||||
create_social_account_dialog_result_free(result);
|
||||
return;
|
||||
}
|
||||
input.platform = create_social_account_dialog_result_get_platform(result);
|
||||
input.profile_url = create_social_account_dialog_result_get_profile_url(result);
|
||||
input.username = create_social_account_dialog_result_get_username(result);
|
||||
input.platform_identifier = create_social_account_dialog_result_get_platform_identifier(result);
|
||||
input.first_observed_at = create_social_account_dialog_result_get_first_observed_at(result);
|
||||
input.account_state = create_social_account_dialog_result_get_account_state(result);
|
||||
input.notes = create_social_account_dialog_result_get_notes(result);
|
||||
input.evidence_identifier = create_social_account_dialog_result_get_evidence_identifier(result);
|
||||
database = investigation_session_get_database(application->session);
|
||||
if (!social_account_service_create(database, &input, &identifier, &error))
|
||||
{
|
||||
application_present_error(application, "Compte social non créé",
|
||||
error != NULL ? error->message : "L'écriture dans l'enquête a échoué.");
|
||||
}
|
||||
else
|
||||
{
|
||||
project = investigation_session_get_project(application->session);
|
||||
application_message_dialog_present(
|
||||
main_window_get_window(application->main_window),
|
||||
APPLICATION_MESSAGE_DIALOG_INFORMATION,
|
||||
"Compte social ajouté",
|
||||
"Le profil et son éventuelle preuve associée ont été ajoutés au graphe.");
|
||||
application_start_graph_loading(application,
|
||||
investigation_project_get_database_path(project));
|
||||
}
|
||||
g_clear_error(&error);
|
||||
g_free(identifier);
|
||||
create_social_account_dialog_result_free(result);
|
||||
}
|
||||
|
||||
/** @brief Charge les preuves puis ouvre le formulaire de compte social. */
|
||||
static void application_on_add_social_account_requested(gpointer user_data)
|
||||
{
|
||||
Application *application = user_data;
|
||||
Database *database = NULL;
|
||||
EvidenceDao *dao = NULL;
|
||||
GPtrArray *evidence_records = NULL;
|
||||
GError *error = NULL;
|
||||
if (application == NULL || application->main_window == NULL ||
|
||||
application->session == NULL) return;
|
||||
database = investigation_session_get_database(application->session);
|
||||
dao = evidence_dao_new(database, &error);
|
||||
if (dao != NULL) evidence_records = evidence_dao_list_all(dao, &error);
|
||||
if (evidence_records == NULL)
|
||||
{
|
||||
application_present_error(application, "Formulaire indisponible",
|
||||
error != NULL ? error->message : "Impossible de charger les preuves.");
|
||||
}
|
||||
else
|
||||
{
|
||||
create_social_account_dialog_present(
|
||||
main_window_get_window(application->main_window), evidence_records,
|
||||
application_on_social_account_completed, application);
|
||||
}
|
||||
g_clear_pointer(&evidence_records, g_ptr_array_unref);
|
||||
evidence_dao_free(dao);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Traite la sélection d'un nœud dans l'arborescence.
|
||||
*
|
||||
|
|
@ -6147,11 +6228,22 @@ static void application_on_activate(
|
|||
application
|
||||
);
|
||||
|
||||
main_window_set_add_social_account_callback(
|
||||
application->main_window,
|
||||
application_on_add_social_account_requested,
|
||||
application
|
||||
);
|
||||
|
||||
main_window_set_import_evidence_enabled(
|
||||
application->main_window,
|
||||
application->session != NULL
|
||||
);
|
||||
|
||||
main_window_set_add_social_account_enabled(
|
||||
application->main_window,
|
||||
application->session != NULL
|
||||
);
|
||||
|
||||
main_window_set_quit_callback(
|
||||
application->main_window,
|
||||
application_on_quit_requested,
|
||||
|
|
|
|||
153
src/core/social_account_service.c
Normal file
153
src/core/social_account_service.c
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/******************************************************************************
|
||||
* @file social_account_service.c
|
||||
* @brief Création transactionnelle d'entités de comptes sociaux.
|
||||
******************************************************************************/
|
||||
|
||||
#include "core/social_account_service.h"
|
||||
|
||||
#include "dao/entity_dao.h"
|
||||
#include "dao/evidence_entity_dao.h"
|
||||
#include "database/statement.h"
|
||||
#include "database/transaction.h"
|
||||
#include "models/entity_record.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/** @brief Domaine d'erreur privé du service. */
|
||||
#define SOCIAL_ACCOUNT_SERVICE_ERROR social_account_service_error_quark()
|
||||
|
||||
/** @brief Retourne le domaine d'erreur du service. */
|
||||
static GQuark social_account_service_error_quark(void)
|
||||
{
|
||||
return g_quark_from_static_string("social-account-service-error");
|
||||
}
|
||||
|
||||
/** @brief Associe une plateforme au type d'entité correspondant. */
|
||||
static const char *social_account_service_entity_type(const char *platform)
|
||||
{
|
||||
if (g_strcmp0(platform, "tiktok") == 0) return "tiktok_account";
|
||||
if (g_strcmp0(platform, "instagram") == 0) return "instagram_account";
|
||||
if (g_strcmp0(platform, "facebook") == 0) return "facebook_account";
|
||||
if (g_strcmp0(platform, "x") == 0) return "x_account";
|
||||
if (g_strcmp0(platform, "telegram") == 0) return "telegram_account";
|
||||
if (g_strcmp0(platform, "other") == 0) return "social_account";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/** @brief Indique si l'état du compte est pris en charge. */
|
||||
static gboolean social_account_service_state_is_valid(const char *state)
|
||||
{
|
||||
return g_strcmp0(state, "active") == 0 ||
|
||||
g_strcmp0(state, "private") == 0 ||
|
||||
g_strcmp0(state, "suspended") == 0 ||
|
||||
g_strcmp0(state, "deleted") == 0 ||
|
||||
g_strcmp0(state, "unknown") == 0;
|
||||
}
|
||||
|
||||
/** @brief Lie un texte ou SQL NULL à une requête. */
|
||||
static gboolean social_account_service_bind_optional(
|
||||
DatabaseStatement *statement, int index, const char *value)
|
||||
{
|
||||
return value != NULL && value[0] != '\0'
|
||||
? database_statement_bind_text(statement, index, value)
|
||||
: database_statement_bind_null(statement, index);
|
||||
}
|
||||
|
||||
gboolean social_account_service_create(
|
||||
Database *database,
|
||||
const SocialAccountInput *input,
|
||||
char **out_entity_identifier,
|
||||
GError **error)
|
||||
{
|
||||
static const char *const insert_sql =
|
||||
"INSERT INTO comptes_sociaux"
|
||||
"(entite_id, plateforme, url_profil, pseudonyme, "
|
||||
"identifiant_plateforme, premiere_observation, etat_compte, notes) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
EntityDao *entity_dao = NULL;
|
||||
EvidenceEntityDao *link_dao = NULL;
|
||||
DatabaseStatement *statement = NULL;
|
||||
EntityRecord *record = NULL;
|
||||
GDateTime *parsed_date = NULL;
|
||||
char *identifier = NULL;
|
||||
char *now_text = NULL;
|
||||
const char *type = NULL;
|
||||
gboolean transaction_active = FALSE;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (out_entity_identifier != NULL) *out_entity_identifier = NULL;
|
||||
type = input != NULL
|
||||
? social_account_service_entity_type(input->platform) : NULL;
|
||||
parsed_date = input != NULL && input->first_observed_at != NULL
|
||||
? g_date_time_new_from_iso8601(input->first_observed_at, NULL) : NULL;
|
||||
if (database == NULL || input == NULL || type == NULL ||
|
||||
input->profile_url == NULL ||
|
||||
(!g_str_has_prefix(input->profile_url, "https://") &&
|
||||
!g_str_has_prefix(input->profile_url, "http://")) ||
|
||||
input->username == NULL || input->username[0] == '\0' ||
|
||||
parsed_date == NULL ||
|
||||
!social_account_service_state_is_valid(input->account_state))
|
||||
{
|
||||
g_set_error_literal(error, SOCIAL_ACCOUNT_SERVICE_ERROR, 1,
|
||||
"Les informations du compte social sont invalides.");
|
||||
goto cleanup;
|
||||
}
|
||||
identifier = g_uuid_string_random();
|
||||
{
|
||||
GDateTime *now = g_date_time_new_now_utc();
|
||||
now_text = now != NULL ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||
g_clear_pointer(&now, g_date_time_unref);
|
||||
}
|
||||
if (identifier == NULL || now_text == NULL ||
|
||||
!database_transaction_begin(database)) goto cleanup;
|
||||
transaction_active = TRUE;
|
||||
entity_dao = entity_dao_new(database, error);
|
||||
record = entity_record_new(identifier, type, input->profile_url,
|
||||
input->username, input->notes, 50, now_text, now_text,
|
||||
ENTITY_STATUS_ACTIVE, error);
|
||||
if (entity_dao == NULL || record == NULL ||
|
||||
!entity_dao_insert(entity_dao, record, error)) goto cleanup;
|
||||
statement = database_statement_prepare(database, insert_sql);
|
||||
if (statement == NULL ||
|
||||
!database_statement_bind_text(statement, 1, identifier) ||
|
||||
!database_statement_bind_text(statement, 2, input->platform) ||
|
||||
!database_statement_bind_text(statement, 3, input->profile_url) ||
|
||||
!database_statement_bind_text(statement, 4, input->username) ||
|
||||
!social_account_service_bind_optional(statement, 5, input->platform_identifier) ||
|
||||
!database_statement_bind_text(statement, 6, input->first_observed_at) ||
|
||||
!database_statement_bind_text(statement, 7, input->account_state) ||
|
||||
!social_account_service_bind_optional(statement, 8, input->notes) ||
|
||||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||
{
|
||||
if (error != NULL && *error == NULL)
|
||||
g_set_error_literal(error, SOCIAL_ACCOUNT_SERVICE_ERROR, 2,
|
||||
"Impossible d'enregistrer la fiche du compte social.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (input->evidence_identifier != NULL &&
|
||||
input->evidence_identifier[0] != '\0')
|
||||
{
|
||||
link_dao = evidence_entity_dao_new(database, error);
|
||||
if (link_dao == NULL || !evidence_entity_dao_link(
|
||||
link_dao, input->evidence_identifier, identifier, error))
|
||||
goto cleanup;
|
||||
}
|
||||
if (!database_transaction_commit(database)) goto cleanup;
|
||||
transaction_active = FALSE;
|
||||
success = TRUE;
|
||||
if (out_entity_identifier != NULL)
|
||||
*out_entity_identifier = g_strdup(identifier);
|
||||
|
||||
cleanup:
|
||||
if (!success && transaction_active)
|
||||
database_transaction_rollback(database);
|
||||
database_statement_finalize(statement);
|
||||
evidence_entity_dao_free(link_dao);
|
||||
entity_record_free(record);
|
||||
entity_dao_free(entity_dao);
|
||||
g_clear_pointer(&parsed_date, g_date_time_unref);
|
||||
g_free(now_text);
|
||||
g_free(identifier);
|
||||
return success;
|
||||
}
|
||||
|
|
@ -16,12 +16,12 @@
|
|||
/**
|
||||
* @brief Version actuelle du schéma SQLite.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 3
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 4
|
||||
|
||||
/**
|
||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "3"
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "4"
|
||||
|
||||
/**
|
||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||
|
|
@ -692,6 +692,26 @@ rollback:
|
|||
return false;
|
||||
}
|
||||
|
||||
/** @brief Applique atomiquement la migration du schéma V3 vers V4. */
|
||||
static bool database_migrate_v3_to_v4(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
|
||||
if (database == NULL || !database_transaction_begin(database))
|
||||
return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v4(database) ||
|
||||
!database_update_schema_version(database, "4") ||
|
||||
!database_transaction_commit(database))
|
||||
goto rollback;
|
||||
return true;
|
||||
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V3 vers V4.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||
*/
|
||||
|
|
@ -884,6 +904,12 @@ bool database_migrate_to_latest(
|
|||
schema_version = 3;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (!database_migrate_v3_to_v4(database))
|
||||
return false;
|
||||
schema_version = 4;
|
||||
break;
|
||||
|
||||
default:
|
||||
database_set_error(
|
||||
database,
|
||||
|
|
@ -1000,6 +1026,11 @@ bool database_initialize(
|
|||
goto rollback;
|
||||
}
|
||||
|
||||
if (!schema_install_v4(database))
|
||||
{
|
||||
goto rollback;
|
||||
}
|
||||
|
||||
if (!schema_ensure_current(
|
||||
database
|
||||
))
|
||||
|
|
|
|||
|
|
@ -204,6 +204,17 @@ bool schema_install_v3(
|
|||
);
|
||||
}
|
||||
|
||||
bool schema_install_v4(
|
||||
Database *database
|
||||
)
|
||||
{
|
||||
return schema_execute_file(
|
||||
database,
|
||||
"database/schema_v4.sql",
|
||||
"la migration SQLite V4"
|
||||
);
|
||||
}
|
||||
|
||||
bool schema_ensure_current(
|
||||
Database *database
|
||||
)
|
||||
|
|
|
|||
277
src/views/create_social_account_dialog.c
Normal file
277
src/views/create_social_account_dialog.c
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
/******************************************************************************
|
||||
* @file create_social_account_dialog.c
|
||||
* @brief Dialogue GTK de création d'un compte social observé.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/create_social_account_dialog.h"
|
||||
|
||||
struct CreateSocialAccountDialogResult
|
||||
{
|
||||
char *platform;
|
||||
char *profile_url;
|
||||
char *username;
|
||||
char *platform_identifier;
|
||||
char *first_observed_at;
|
||||
char *account_state;
|
||||
char *notes;
|
||||
char *evidence_identifier;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
GtkDropDown *platform;
|
||||
GtkEntry *url;
|
||||
GtkEntry *username;
|
||||
GtkEntry *platform_identifier;
|
||||
GtkEntry *observed_at;
|
||||
GtkDropDown *state;
|
||||
GtkTextView *notes;
|
||||
GtkDropDown *evidence;
|
||||
GtkLabel *error;
|
||||
GPtrArray *evidence_identifiers;
|
||||
CreateSocialAccountDialogCallback callback;
|
||||
gpointer user_data;
|
||||
gboolean completed;
|
||||
} CreateSocialAccountDialogState;
|
||||
|
||||
static const char *const platform_codes[] = {
|
||||
"tiktok", "instagram", "facebook", "x", "telegram", "other"
|
||||
};
|
||||
static const char *const state_codes[] = {
|
||||
"unknown", "active", "private", "suspended", "deleted"
|
||||
};
|
||||
|
||||
/** @brief Retourne une copie nettoyée, ou NULL pour un texte vide. */
|
||||
static char *create_social_account_dialog_copy(const char *text)
|
||||
{
|
||||
char *copy = text != NULL ? g_strdup(text) : NULL;
|
||||
if (copy == NULL) return NULL;
|
||||
g_strstrip(copy);
|
||||
if (copy[0] == '\0') { g_free(copy); return NULL; }
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** @brief Extrait le texte du champ de notes. */
|
||||
static char *create_social_account_dialog_notes(CreateSocialAccountDialogState *state)
|
||||
{
|
||||
GtkTextBuffer *buffer = gtk_text_view_get_buffer(state->notes);
|
||||
GtkTextIter start;
|
||||
GtkTextIter end;
|
||||
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
||||
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
||||
}
|
||||
|
||||
/** @brief Libère l'état privé attaché à la fenêtre. */
|
||||
static void create_social_account_dialog_state_free(gpointer user_data)
|
||||
{
|
||||
CreateSocialAccountDialogState *state = user_data;
|
||||
if (state == NULL) return;
|
||||
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
|
||||
g_free(state);
|
||||
}
|
||||
|
||||
/** @brief Signale une annulation au callback une seule fois. */
|
||||
static void create_social_account_dialog_cancel(CreateSocialAccountDialogState *state)
|
||||
{
|
||||
if (state == NULL || state->completed) return;
|
||||
state->completed = TRUE;
|
||||
if (state->callback != NULL) state->callback(NULL, state->user_data);
|
||||
}
|
||||
|
||||
/** @brief Traite la fermeture native du dialogue. */
|
||||
static gboolean create_social_account_dialog_on_close(GtkWindow *window, gpointer user_data)
|
||||
{
|
||||
(void) window;
|
||||
create_social_account_dialog_cancel(user_data);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/** @brief Traite le bouton d'annulation. */
|
||||
static void create_social_account_dialog_on_cancel(GtkButton *button, gpointer user_data)
|
||||
{
|
||||
CreateSocialAccountDialogState *state = user_data;
|
||||
(void) button;
|
||||
create_social_account_dialog_cancel(state);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
|
||||
/** @brief Valide le formulaire et transmet un résultat possédé. */
|
||||
static void create_social_account_dialog_on_create(GtkButton *button, gpointer user_data)
|
||||
{
|
||||
CreateSocialAccountDialogState *state = user_data;
|
||||
CreateSocialAccountDialogResult *result = NULL;
|
||||
GDateTime *date = NULL;
|
||||
char *notes = NULL;
|
||||
guint platform = gtk_drop_down_get_selected(state->platform);
|
||||
guint account_state = gtk_drop_down_get_selected(state->state);
|
||||
guint evidence = gtk_drop_down_get_selected(state->evidence);
|
||||
const char *url = gtk_editable_get_text(GTK_EDITABLE(state->url));
|
||||
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)
|
||||
{
|
||||
gtk_label_set_text(state->error,
|
||||
"Renseignez une URL http(s), un pseudonyme et une date ISO 8601 valide.");
|
||||
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);
|
||||
result = g_new0(CreateSocialAccountDialogResult, 1);
|
||||
result->platform = g_strdup(platform_codes[platform]);
|
||||
result->profile_url = create_social_account_dialog_copy(url);
|
||||
result->username = create_social_account_dialog_copy(username);
|
||||
result->platform_identifier = create_social_account_dialog_copy(
|
||||
gtk_editable_get_text(GTK_EDITABLE(state->platform_identifier)));
|
||||
result->first_observed_at = create_social_account_dialog_copy(observed_at);
|
||||
result->account_state = g_strdup(state_codes[account_state]);
|
||||
result->notes = create_social_account_dialog_copy(notes);
|
||||
if (evidence > 0 && evidence - 1 < state->evidence_identifiers->len)
|
||||
result->evidence_identifier = g_strdup(g_ptr_array_index(
|
||||
state->evidence_identifiers, evidence - 1));
|
||||
state->completed = TRUE;
|
||||
if (state->callback != NULL) state->callback(result, state->user_data);
|
||||
else create_social_account_dialog_result_free(result);
|
||||
g_free(notes);
|
||||
g_date_time_unref(date);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
|
||||
/** @brief Ajoute une ligne libellée au formulaire. */
|
||||
static void create_social_account_dialog_add_row(
|
||||
GtkGrid *grid, int row, const char *label, GtkWidget *field)
|
||||
{
|
||||
GtkWidget *caption = gtk_label_new(label);
|
||||
gtk_label_set_xalign(GTK_LABEL(caption), 0.0f);
|
||||
gtk_widget_set_halign(caption, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(field, TRUE);
|
||||
gtk_grid_attach(grid, caption, 0, row, 1, 1);
|
||||
gtk_grid_attach(grid, field, 1, row, 1, 1);
|
||||
}
|
||||
|
||||
gboolean create_social_account_dialog_present(
|
||||
GtkWindow *parent, const GPtrArray *evidence_records,
|
||||
CreateSocialAccountDialogCallback callback, gpointer user_data)
|
||||
{
|
||||
static const char *const platform_labels[] = {
|
||||
"TikTok", "Instagram", "Facebook", "X", "Telegram", "Autre", NULL
|
||||
};
|
||||
static const char *const state_labels[] = {
|
||||
"Inconnu", "Actif", "Privé", "Suspendu", "Supprimé", NULL
|
||||
};
|
||||
CreateSocialAccountDialogState *state = NULL;
|
||||
GtkWidget *box = NULL;
|
||||
GtkWidget *grid = NULL;
|
||||
GtkWidget *actions = NULL;
|
||||
GtkWidget *cancel = NULL;
|
||||
GtkWidget *create = NULL;
|
||||
GtkStringList *evidence_labels = NULL;
|
||||
GDateTime *now = NULL;
|
||||
char *now_text = NULL;
|
||||
guint index = 0;
|
||||
if (parent == NULL) return FALSE;
|
||||
state = g_new0(CreateSocialAccountDialogState, 1);
|
||||
state->callback = callback;
|
||||
state->user_data = user_data;
|
||||
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Ajouter un compte social");
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
gtk_window_set_default_size(state->window, 620, 560);
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
|
||||
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
|
||||
grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
state->platform = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(platform_labels));
|
||||
state->url = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->url, "https://www.tiktok.com/@compte");
|
||||
state->username = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->username, "@pseudonyme affiché");
|
||||
state->platform_identifier = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->platform_identifier, "Identifiant stable, si visible");
|
||||
state->observed_at = GTK_ENTRY(gtk_entry_new());
|
||||
now = g_date_time_new_now_utc();
|
||||
now_text = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
|
||||
gtk_editable_set_text(GTK_EDITABLE(state->observed_at), now_text);
|
||||
state->state = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(state_labels));
|
||||
evidence_labels = gtk_string_list_new(NULL);
|
||||
gtk_string_list_append(evidence_labels, "Aucune preuve associée");
|
||||
for (index = 0; evidence_records != NULL && index < evidence_records->len; index++)
|
||||
{
|
||||
EvidenceRecord *record = g_ptr_array_index((GPtrArray *) evidence_records, index);
|
||||
const char *identifier = evidence_record_get_identifier(record);
|
||||
const char *name = evidence_record_get_original_name(record);
|
||||
if (identifier == NULL || name == NULL) continue;
|
||||
gtk_string_list_append(evidence_labels, name);
|
||||
g_ptr_array_add(state->evidence_identifiers, g_strdup(identifier));
|
||||
}
|
||||
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||
G_LIST_MODEL(evidence_labels), NULL));
|
||||
g_object_unref(evidence_labels);
|
||||
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);
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 0, "Plateforme", GTK_WIDGET(state->platform));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 1, "URL complète", GTK_WIDGET(state->url));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->username));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 3, "ID plateforme", GTK_WIDGET(state->platform_identifier));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 4, "Première observation (UTC)", GTK_WIDGET(state->observed_at));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 5, "État observé", GTK_WIDGET(state->state));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 6, "Preuve associée", GTK_WIDGET(state->evidence));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 7, "Notes factuelles", GTK_WIDGET(state->notes));
|
||||
state->error = GTK_LABEL(gtk_label_new(NULL));
|
||||
gtk_label_set_wrap(state->error, TRUE);
|
||||
gtk_widget_add_css_class(GTK_WIDGET(state->error), "error");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
cancel = gtk_button_new_with_label("Annuler");
|
||||
create = gtk_button_new_with_label("Ajouter au graphe");
|
||||
gtk_widget_add_css_class(create, "suggested-action");
|
||||
gtk_box_append(GTK_BOX(actions), cancel); gtk_box_append(GTK_BOX(actions), create);
|
||||
gtk_box_append(GTK_BOX(box), grid);
|
||||
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
|
||||
gtk_box_append(GTK_BOX(box), actions);
|
||||
gtk_window_set_child(state->window, box);
|
||||
g_signal_connect(state->window, "close-request", G_CALLBACK(create_social_account_dialog_on_close), state);
|
||||
g_signal_connect(cancel, "clicked", G_CALLBACK(create_social_account_dialog_on_cancel), state);
|
||||
g_signal_connect(create, "clicked", G_CALLBACK(create_social_account_dialog_on_create), state);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "social-account-state", state,
|
||||
create_social_account_dialog_state_free);
|
||||
gtk_window_present(state->window);
|
||||
g_free(now_text); g_date_time_unref(now);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void create_social_account_dialog_result_free(CreateSocialAccountDialogResult *result)
|
||||
{
|
||||
if (result == NULL) return;
|
||||
g_free(result->platform); g_free(result->profile_url); g_free(result->username);
|
||||
g_free(result->platform_identifier); g_free(result->first_observed_at);
|
||||
g_free(result->account_state); g_free(result->notes);
|
||||
g_free(result->evidence_identifier); g_free(result);
|
||||
}
|
||||
|
||||
#define SOCIAL_RESULT_GETTER(name, field) \
|
||||
const char *create_social_account_dialog_result_get_##name( \
|
||||
const CreateSocialAccountDialogResult *result) \
|
||||
{ return result != NULL ? result->field : NULL; }
|
||||
|
||||
SOCIAL_RESULT_GETTER(platform, platform)
|
||||
SOCIAL_RESULT_GETTER(profile_url, profile_url)
|
||||
SOCIAL_RESULT_GETTER(username, username)
|
||||
SOCIAL_RESULT_GETTER(platform_identifier, platform_identifier)
|
||||
SOCIAL_RESULT_GETTER(first_observed_at, first_observed_at)
|
||||
SOCIAL_RESULT_GETTER(account_state, account_state)
|
||||
SOCIAL_RESULT_GETTER(notes, notes)
|
||||
SOCIAL_RESULT_GETTER(evidence_identifier, evidence_identifier)
|
||||
|
|
@ -59,6 +59,7 @@ struct MainWindow
|
|||
GtkWidget *new_investigation_button;
|
||||
GtkWidget *open_investigation_button;
|
||||
GtkWidget *import_evidence_button;
|
||||
GtkWidget *add_social_account_button;
|
||||
GtkWidget *show_graph_button;
|
||||
GtkWidget *content_paned;
|
||||
GtkWidget *main_paned;
|
||||
|
|
@ -87,6 +88,9 @@ struct MainWindow
|
|||
gpointer
|
||||
import_evidence_user_data;
|
||||
|
||||
MainWindowAddSocialAccountCallback add_social_account_callback;
|
||||
gpointer add_social_account_user_data;
|
||||
|
||||
MainWindowShowGraphCallback
|
||||
show_graph_callback;
|
||||
|
||||
|
|
@ -294,6 +298,18 @@ static void main_window_on_import_evidence_clicked(
|
|||
);
|
||||
}
|
||||
|
||||
/** @brief Relaie la demande d'ajout d'un compte social. */
|
||||
static void main_window_on_add_social_account_clicked(
|
||||
GtkButton *button, gpointer user_data)
|
||||
{
|
||||
MainWindow *main_window = user_data;
|
||||
(void) button;
|
||||
if (main_window == NULL || main_window->add_social_account_callback == NULL)
|
||||
return;
|
||||
main_window->add_social_account_callback(
|
||||
main_window->add_social_account_user_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transmet la demande de fermeture au contrôleur.
|
||||
*
|
||||
|
|
@ -594,6 +610,12 @@ MainWindow *main_window_new(
|
|||
"Importer une preuve"
|
||||
);
|
||||
|
||||
main_window->add_social_account_button =
|
||||
main_window_create_action_button(
|
||||
"contact-new-symbolic",
|
||||
"Ajouter un compte social"
|
||||
);
|
||||
|
||||
main_window->show_graph_button =
|
||||
main_window_create_action_button(
|
||||
"go-previous-symbolic",
|
||||
|
|
@ -625,6 +647,8 @@ MainWindow *main_window_new(
|
|||
FALSE
|
||||
);
|
||||
|
||||
gtk_widget_set_sensitive(main_window->add_social_account_button, FALSE);
|
||||
|
||||
gtk_widget_set_sensitive(
|
||||
main_window->show_graph_button,
|
||||
FALSE
|
||||
|
|
@ -645,6 +669,9 @@ MainWindow *main_window_new(
|
|||
main_window->import_evidence_button
|
||||
);
|
||||
|
||||
gtk_box_append(GTK_BOX(main_window->action_bar),
|
||||
main_window->add_social_account_button);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(main_window->action_bar),
|
||||
main_window->show_graph_button
|
||||
|
|
@ -687,6 +714,9 @@ MainWindow *main_window_new(
|
|||
main_window
|
||||
);
|
||||
|
||||
g_signal_connect(main_window->add_social_account_button, "clicked",
|
||||
G_CALLBACK(main_window_on_add_social_account_clicked), main_window);
|
||||
|
||||
g_signal_connect(
|
||||
main_window->show_graph_button,
|
||||
"clicked",
|
||||
|
|
@ -1411,6 +1441,16 @@ void main_window_set_import_evidence_callback(
|
|||
user_data;
|
||||
}
|
||||
|
||||
void main_window_set_add_social_account_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowAddSocialAccountCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (main_window == NULL) return;
|
||||
main_window->add_social_account_callback = callback;
|
||||
main_window->add_social_account_user_data = user_data;
|
||||
}
|
||||
|
||||
void main_window_set_show_graph_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowShowGraphCallback callback,
|
||||
|
|
@ -1446,6 +1486,14 @@ void main_window_set_import_evidence_enabled(
|
|||
);
|
||||
}
|
||||
|
||||
void main_window_set_add_social_account_enabled(
|
||||
MainWindow *main_window, gboolean enabled)
|
||||
{
|
||||
if (main_window == NULL || main_window->add_social_account_button == NULL)
|
||||
return;
|
||||
gtk_widget_set_sensitive(main_window->add_social_account_button, enabled);
|
||||
}
|
||||
|
||||
void main_window_set_verify_evidence_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowVerifyEvidenceCallback callback,
|
||||
|
|
|
|||
|
|
@ -525,10 +525,11 @@ static void test_database_initialize_valid_database(void)
|
|||
"FROM investigation;"
|
||||
);
|
||||
|
||||
assert(strcmp(schema_version, "3") == 0);
|
||||
assert(strcmp(schema_version, "4") == 0);
|
||||
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_relations");
|
||||
test_database_assert_table_exists(database, "comptes_sociaux");
|
||||
assert(strcmp(application_name, "Labfy Investigation") == 0);
|
||||
|
||||
assert(created_at[0] != '\0');
|
||||
|
|
@ -987,7 +988,7 @@ static void test_database_migrate_v1_to_v2(void)
|
|||
assert(
|
||||
strcmp(
|
||||
schema_version,
|
||||
"3"
|
||||
"4"
|
||||
) == 0
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -245,7 +245,11 @@ static void test_entity_type_dao_list_all(void)
|
|||
"domain_name",
|
||||
"ip_address",
|
||||
"organization",
|
||||
"other"
|
||||
"other",
|
||||
"tiktok_account",
|
||||
"x_account",
|
||||
"telegram_account",
|
||||
"social_account"
|
||||
};
|
||||
|
||||
static const char *const expected_labels[] =
|
||||
|
|
@ -263,7 +267,11 @@ static void test_entity_type_dao_list_all(void)
|
|||
"Nom de domaine",
|
||||
"Adresse IP",
|
||||
"Organisation",
|
||||
"Autre"
|
||||
"Autre",
|
||||
"Compte TikTok",
|
||||
"Compte X",
|
||||
"Compte Telegram",
|
||||
"Autre compte social"
|
||||
};
|
||||
|
||||
TestEntityTypeDaoFixture fixture =
|
||||
|
|
|
|||
75
tests/test_social_account_service.c
Normal file
75
tests/test_social_account_service.c
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/******************************************************************************
|
||||
* @file test_social_account_service.c
|
||||
* @brief Tests transactionnels de création des comptes sociaux.
|
||||
******************************************************************************/
|
||||
|
||||
#include "core/social_account_service.h"
|
||||
#include "database/database.h"
|
||||
#include "database/statement.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
/** @brief Lit un comptage entier dans la base de test. */
|
||||
static gint64 test_social_account_count(Database *database, const char *sql)
|
||||
{
|
||||
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||
int64_t count = -1;
|
||||
assert(statement != NULL);
|
||||
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
|
||||
assert(database_statement_column_int64(statement, 0, &count));
|
||||
database_statement_finalize(statement);
|
||||
return count;
|
||||
}
|
||||
|
||||
/** @brief Vérifie la création et le rollback d'un doublon. */
|
||||
static void test_social_account_create_and_duplicate(void)
|
||||
{
|
||||
char *directory = NULL;
|
||||
char *path = NULL;
|
||||
char *identifier = NULL;
|
||||
Database *database = NULL;
|
||||
GError *error = NULL;
|
||||
SocialAccountInput input = {
|
||||
.platform = "tiktok",
|
||||
.profile_url = "https://www.tiktok.com/@profil.test",
|
||||
.username = "@profil.test",
|
||||
.platform_identifier = "123456789",
|
||||
.first_observed_at = "2026-07-22T10:00:00Z",
|
||||
.account_state = "active",
|
||||
.notes = "URL transmise par la victime.",
|
||||
.evidence_identifier = NULL
|
||||
};
|
||||
directory = g_dir_make_tmp("labfy-social-account-test-XXXXXX", &error);
|
||||
assert(directory != NULL && error == NULL);
|
||||
path = g_build_filename(directory, "Enquete.sqlite", NULL);
|
||||
assert(database_initialize(path, "Enquete_Social", directory));
|
||||
database = database_open(path);
|
||||
assert(database != NULL);
|
||||
assert(database_migrate_to_latest(database));
|
||||
assert(social_account_service_create(database, &input, &identifier, &error));
|
||||
assert(identifier != NULL && error == NULL);
|
||||
assert(test_social_account_count(database, "SELECT COUNT(*) FROM entites;") == 1);
|
||||
assert(test_social_account_count(database, "SELECT COUNT(*) FROM comptes_sociaux;") == 1);
|
||||
g_clear_pointer(&identifier, g_free);
|
||||
assert(!social_account_service_create(database, &input, &identifier, &error));
|
||||
assert(error != NULL);
|
||||
assert(test_social_account_count(database, "SELECT COUNT(*) FROM entites;") == 1);
|
||||
assert(test_social_account_count(database, "SELECT COUNT(*) FROM comptes_sociaux;") == 1);
|
||||
g_clear_error(&error);
|
||||
database_close(database);
|
||||
assert(g_remove(path) == 0);
|
||||
assert(g_rmdir(directory) == 0);
|
||||
g_free(path);
|
||||
g_free(directory);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_social_account_create_and_duplicate();
|
||||
puts("SocialAccountService : tous les tests sont valides.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue