diff --git a/.htaccess b/.htaccess index b9e5b64..3d07569 100644 --- a/.htaccess +++ b/.htaccess @@ -3,58 +3,75 @@ # Activer le moteur de réécriture RewriteEngine On -# Protéger les fichiers sensibles -<FilesMatch "^(config\.php|functions\.php)$"> - Order Allow,Deny - Deny from all -</FilesMatch> +# Forcer HTTPS (à activer en production en supprimant le commentaire) +# RewriteCond %{HTTPS} !=on +# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] -# Bloquer l'accès au répertoire includes +# Protéger le répertoire includes <IfModule mod_rewrite.c> RewriteRule ^includes/ - [F,L] </IfModule> -# Bloquer l'accès aux fichiers cachés +# Bloquer l'accès aux fichiers sensibles +<FilesMatch "^(config\.php|functions\.php|app_data\.php)$"> + Order Allow,Deny + Deny from all +</FilesMatch> + +# Protéger .htaccess et tout fichier commençant par un point <FilesMatch "^\."> Order Allow,Deny Deny from all </FilesMatch> -# Forcer HTTPS (décommenter en production) -# RewriteCond %{HTTPS} !=on -# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - -# Headers de sécurité -<IfModule mod_headers.c> - # Empêcher le clickjacking - Header set X-Frame-Options "SAMEORIGIN" - - # Prévention XSS - Header set X-XSS-Protection "1; mode=block" - - # Empêcher le MIME sniffing - Header set X-Content-Type-Options "nosniff" - - # Référer policy - Header set Referrer-Policy "strict-origin-when-cross-origin" - - # Content Security Policy (CSP) - Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self';" -</IfModule> - # Désactiver l'affichage du contenu des répertoires Options -Indexes -# Limiter les méthodes HTTP +# Limiter les méthodes HTTP autorisées <LimitExcept GET POST HEAD> Order Allow,Deny Deny from all </LimitExcept> -# PHP settings +# Headers de sécurité +<IfModule mod_headers.c> + # Protection contre le clickjacking + Header always set X-Frame-Options "SAMEORIGIN" + + # Protection XSS + Header always set X-XSS-Protection "1; mode=block" + + # Prévention MIME sniffing + Header always set X-Content-Type-Options "nosniff" + + # Referrer Policy + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + # Content Security Policy - Ajusté pour les ressources externes utilisées + Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'" + + # Désactiver la détection automatique du cache + Header unset ETag + FileETag None + + # Mise en cache des ressources statiques + <FilesMatch "\.(css|js)$"> + Header set Cache-Control "max-age=604800, public" + </FilesMatch> + + # En production, activer HSTS (HTTP Strict Transport Security) + # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=HTTPS +</IfModule> + +# Configuration PHP <IfModule mod_php.c> - # Désactiver l'affichage des erreurs en production + # Masquer la version de PHP et autres informations + php_flag expose_php Off + + # Désactiver l'affichage des erreurs en production (à décommenter en production) # php_flag display_errors Off + # php_flag display_startup_errors Off + # php_value error_reporting 0 # Limiter le temps d'exécution des scripts php_value max_execution_time 120 @@ -63,10 +80,22 @@ Options -Indexes php_value upload_max_filesize 10M php_value post_max_size 10M - # Sécuriser les cookies de session + # Sécurité des sessions php_value session.cookie_httponly 1 php_value session.use_only_cookies 1 + php_value session.cookie_samesite "Lax" - # Utiliser des cookies sécurisés en production + # Utiliser des cookies sécurisés en production (à décommenter en production) # php_value session.cookie_secure 1 -</IfModule> \ No newline at end of file +</IfModule> + +# Compresser les fichiers texte pour réduire la taille de transfert +<IfModule mod_deflate.c> + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json +</IfModule> + +# Protection contre les scans de vulnérabilités communes +RedirectMatch 404 (?i)\.php\.suspected +RedirectMatch 404 (?i)wp-login\.php +RedirectMatch 404 (?i)wp-admin +RedirectMatch 404 (?i)xmlrpc\.php \ No newline at end of file diff --git a/callback.php b/callback.php index dbd47e6..e017dbd 100644 --- a/callback.php +++ b/callback.php @@ -40,83 +40,50 @@ if (!isset($_SESSION['misskey_instance']) || empty($_SESSION['misskey_instance'] exit; } -$instance = $_SESSION['misskey_instance']; -$code = $_GET['code']; - -// En production, ici nous échangerions le code contre un token d'accès -// Pour cette version initiale, nous simulons l'échange - -// Simulation : générer un token fictif -// Dans une implémentation réelle, nous appellerions l'API Misskey pour obtenir un vrai token -$_SESSION['misskey_token'] = 'DEMO_' . bin2hex(random_bytes(16)); - -// Dans un cas réel, nous aurions un code similaire à celui-ci: -/* -// Construire l'URL pour l'échange du code -$token_url = "https://{$instance}/oauth/token"; - -// Paramètres de la requête -$params = [ - 'client_id' => $config['client_id'], - 'client_secret' => $config['client_secret'], - 'grant_type' => 'authorization_code', - 'code' => $code, - 'redirect_uri' => $config['app_url'] . '/callback.php' -]; - -// Initialiser cURL -$ch = curl_init(); - -// Configurer la requête -curl_setopt_array($ch, [ - CURLOPT_URL => $token_url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query($params), - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/x-www-form-urlencoded' - ], - CURLOPT_TIMEOUT => 30, - CURLOPT_SSL_VERIFYPEER => true -]); - -// Exécuter la requête -$response = curl_exec($ch); -$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); -$error = curl_error($ch); - -// Fermer la session cURL -curl_close($ch); - -// Vérifier les erreurs -if ($error || $http_code !== 200) { +// Récupérer le client_secret depuis la session +if (!isset($_SESSION['misskey_client_secret']) || empty($_SESSION['misskey_client_secret'])) { $_SESSION['messages'][] = [ 'type' => 'danger', - 'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . ($error ?: 'HTTP ' . $http_code) + 'text' => 'Informations d\'application manquantes. Veuillez recommencer.' ]; header('Location: index.php'); exit; } -// Décoder la réponse -$response_data = json_decode($response, true); +$instance = $_SESSION['misskey_instance']; +$code = $_GET['code']; +$client_secret = $_SESSION['misskey_client_secret']; -// Vérifier que le token est présent -if (!isset($response_data['access_token']) || empty($response_data['access_token'])) { +// Récupérer le client_id depuis les données d'application +if (!isset($app_data['instances'][$instance]) || !isset($app_data['instances'][$instance]['client_id'])) { $_SESSION['messages'][] = [ 'type' => 'danger', - 'text' => 'Aucun token d\'accès reçu. L\'authentification a échoué.' + 'text' => 'Informations d\'application introuvables pour ' . $instance . '. Veuillez recommencer.' + ]; + header('Location: index.php'); + exit; +} + +$client_id = $app_data['instances'][$instance]['client_id']; + +// Échanger le code contre un token d'accès +$exchange_result = exchange_oauth_code($instance, $code, $client_id, $client_secret); + +if (!$exchange_result['success']) { + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . $exchange_result['message'] ]; header('Location: index.php'); exit; } // Stocker le token dans la session -$_SESSION['misskey_token'] = $response_data['access_token']; -*/ +$_SESSION['misskey_token'] = $exchange_result['access_token']; -// Nettoyer l'état OAuth +// Nettoyer les données temporaires de la session unset($_SESSION['oauth_state']); +unset($_SESSION['misskey_client_secret']); // Ajouter un message de succès $_SESSION['messages'][] = [ diff --git a/css/styles.css b/css/styles.css index 9a49066..9bee0c0 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,61 +1,217 @@ -/* FavMasToKey - Styles personnalisés */ +/* FavMasToKey - Thème sombre */ + +:root { + --bg-dark: #121212; + --bg-card: #1e1e1e; + --bg-input: #2a2a2a; + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #8a8a8a; + --primary-color: #7e57c2; + --primary-hover: #9575cd; + --success-color: #4caf50; + --info-color: #29b6f6; + --warning-color: #ffb74d; + --danger-color: #f44336; + --border-color: #333333; + --card-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); +} body { - background-color: #f8f9fa; + background-color: var(--bg-dark); + color: var(--text-primary); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; } .container { max-width: 900px; } -h1 { - color: #563d7c; +h1, h2, h3, h4, h5, h6 { + color: var(--text-primary); margin-bottom: 0.5rem; } +h1 { + color: var(--primary-color); +} + +p { + color: var(--text-secondary); +} + +/* Cards */ +.card { + background-color: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 10px; + box-shadow: var(--card-shadow); + overflow: hidden; +} + +.card-title { + color: var(--primary-color); + border-bottom: 1px solid var(--border-color); + padding-bottom: 1rem; + margin-bottom: 1.5rem; +} + +.card-body { + color: var(--text-primary); +} + +/* Form elements */ +.form-control, .form-select { + background-color: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-primary); +} + +.form-control:focus, .form-select:focus { + background-color: var(--bg-input); + border-color: var(--primary-color); + box-shadow: 0 0 0 0.25rem rgba(126, 87, 194, 0.25); + color: var(--text-primary); +} + +.form-control::placeholder { + color: var(--text-muted); +} + +.form-text { + color: var(--text-muted); +} + +/* Buttons */ +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-primary:hover, .btn-primary:focus { + background-color: var(--primary-hover); + border-color: var(--primary-hover); +} + +.btn-outline-primary { + color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-outline-primary:hover { + background-color: var(--primary-color); + border-color: var(--primary-color); + color: white; +} + +.btn-link { + color: var(--primary-color); +} + +.btn-warning { + background-color: var(--warning-color); + border-color: var(--warning-color); + color: #212529; +} + +.btn-danger { + background-color: var(--danger-color); + border-color: var(--danger-color); +} + +/* Alerts */ +.alert { + border-radius: 8px; + border: none; +} + +.alert-info { + background-color: rgba(41, 182, 246, 0.2); + color: var(--info-color); + border-left: 4px solid var(--info-color); +} + +.alert-success { + background-color: rgba(76, 175, 80, 0.2); + color: var(--success-color); + border-left: 4px solid var(--success-color); +} + +.alert-warning { + background-color: rgba(255, 183, 77, 0.2); + color: var(--warning-color); + border-left: 4px solid var(--warning-color); +} + +.alert-danger { + background-color: rgba(244, 67, 54, 0.2); + color: var(--danger-color); + border-left: 4px solid var(--danger-color); +} + +/* Progress bars */ +.progress { + background-color: var(--border-color); + border-radius: 10px; + height: 15px; + overflow: hidden; +} + +.progress-bar { + background-color: var(--primary-color); + border-radius: 10px; +} + +.progress-bar.bg-info { + background-color: var(--info-color) !important; +} + +/* Log container */ +#log-container { + background-color: #1a1a1a; + border: 1px solid var(--border-color); + border-radius: 5px; + font-family: 'Courier New', monospace; + font-size: 0.85rem; + padding: 10px; + max-height: 200px; + overflow-y: auto; +} + +#operation-log .log-entry { + margin-bottom: 0.5rem; + padding: 2px 5px; + border-radius: 3px; +} + +#operation-log .success { + color: var(--success-color); +} + +#operation-log .error { + color: var(--danger-color); +} + +#operation-log .info { + color: var(--info-color); +} + +#operation-log .warning { + color: var(--warning-color); +} + +/* Background for help sections */ +.bg-light { + background-color: #2a2a2a !important; + color: var(--text-secondary); +} + .step { transition: all 0.3s ease; } -.card { - border-radius: 10px; - overflow: hidden; -} - -.card-title { - color: #563d7c; - border-bottom: 1px solid #e9ecef; - padding-bottom: 1rem; - margin-bottom: 1.5rem; -} - -#log-container { - font-family: 'Courier New', monospace; - font-size: 0.85rem; - border-radius: 5px; -} - -#operation-log .log-entry { - margin-bottom: 0.5rem; -} - -#operation-log .success { - color: #28a745; -} - -#operation-log .error { - color: #dc3545; -} - -#operation-log .info { - color: #17a2b8; -} - -#operation-log .warning { - color: #ffc107; -} - -/* Animation pour montrer le progrès */ +/* Animations */ @keyframes progress-pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } @@ -66,18 +222,43 @@ h1 { animation: progress-pulse 2s infinite; } -/* Styles pour les boutons */ -.btn-primary { - background-color: #563d7c; - border-color: #563d7c; +/* Customizing Bootstrap components */ +.list-group-item { + background-color: var(--bg-card); + border-color: var(--border-color); + color: var(--text-primary); } -.btn-primary:hover, .btn-primary:focus { - background-color: #452d6b; - border-color: #452d6b; +.table { + color: var(--text-primary); } -/* Responsive */ +.table-dark { + --bs-table-bg: var(--bg-card); + --bs-table-striped-bg: #2a2a2a; + --bs-table-border-color: var(--border-color); +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-card); +} + +::-webkit-scrollbar-thumb { + background: #555; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #777; +} + +/* Responsive adjustments */ @media (max-width: 768px) { .container { padding: 1rem; @@ -86,4 +267,8 @@ h1 { .card-body { padding: 1.25rem; } + + h1 { + font-size: 2rem; + } } \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..84b0647 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Script de déploiement simplifié pour FavMasToKey + +# Créer les répertoires nécessaires +mkdir -p includes css js images + +# Définir les permissions +chmod 755 ./ ./includes ./css ./js ./images +chmod 644 ./*.php ./css/*.css ./js/*.js ./.htaccess + +# Créer app_data.php +touch includes/app_data.php +chmod 666 includes/app_data.php + +echo "Déploiement terminé avec succès!" \ No newline at end of file diff --git a/doc.php b/doc.php new file mode 100644 index 0000000..a054ccf --- /dev/null +++ b/doc.php @@ -0,0 +1,147 @@ +<?php +/** + * FavMasToKey - Documentation d'utilisation + */ + +// Définir la constante pour inclure les fichiers +define('FAVMASTOKEY', true); + +// Inclure les fichiers requis +require_once 'includes/config.php'; +?> +<!DOCTYPE html> +<html lang="fr" data-bs-theme="dark"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="theme-color" content="#7e57c2"> + <link rel="icon" href="images/favicon.svg" type="image/svg+xml"> + <meta name="description" content="FavMasToKey - Documentation d'utilisation pour transférer vos favoris de Mastodon vers Misskey"> + <title>Documentation - FavMasToKey</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="css/styles.css"> +</head> +<body> + <div class="container py-5"> + <header class="text-center mb-5"> + <h1>FavMasToKey</h1> + <p class="lead">Documentation d'utilisation</p> + <p><a href="index.php" class="btn btn-primary">Retour à l'application</a></p> + </header> + + <div class="row justify-content-center"> + <div class="col-md-10"> + <div class="card shadow-sm mb-4"> + <div class="card-body"> + <h2 class="card-title">Guide étape par étape</h2> + + <h3>1. Préparation</h3> + <div class="mb-4"> + <h4>1.1 Obtenir vos favoris depuis Mastodon</h4> + <ol> + <li>Connectez-vous à votre compte Mastodon</li> + <li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li> + <li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li> + <li>Une fois le fichier prêt, téléchargez-le</li> + </ol> + + <h4>1.2 Préparer votre compte Misskey</h4> + <ol> + <li>Assurez-vous d'être connecté à votre compte Misskey</li> + <li>Vérifiez que vous avez suffisamment d'espace pour de nouveaux favoris</li> + </ol> + </div> + + <h3>2. Utilisation de FavMasToKey</h3> + <div class="mb-4"> + <h4>2.1 Télécharger le fichier JSON</h4> + <ol> + <li>Sur la page d'accueil de FavMasToKey, cliquez sur "Parcourir" pour sélectionner votre fichier JSON de favoris</li> + <li>Cliquez sur "Analyser le fichier" pour continuer</li> + </ol> + + <h4>2.2 Connexion à votre compte Misskey</h4> + <ol> + <li>Entrez l'URL de votre instance Misskey (ex: misskey.io)</li> + <li>Cliquez sur "Se connecter à Misskey"</li> + <li>Vous serez redirigé vers votre instance Misskey pour autoriser l'application</li> + <li>Suivez les instructions à l'écran pour autoriser FavMasToKey</li> + </ol> + + <h4>2.3 Migration des favoris</h4> + <ol> + <li>Une fois l'autorisation accordée, vous serez redirigé vers l'écran de migration</li> + <li>Cliquez sur "Démarrer la migration" pour commencer le processus</li> + <li>Vous pouvez mettre en pause, reprendre ou annuler la migration à tout moment</li> + <li>Le journal des opérations vous montre l'état de chaque favori traité</li> + </ol> + </div> + + <h3>3. Résolution des problèmes courants</h3> + <div class="mb-4"> + <h4>3.1 Publications non trouvées</h4> + <p>Certaines publications peuvent ne pas être trouvées sur le réseau fédéré pour diverses raisons :</p> + <ul> + <li>La publication a été supprimée</li> + <li>L'instance d'origine est hors ligne</li> + <li>L'utilisateur a changé ses paramètres de confidentialité</li> + <li>Votre instance Misskey ne s'est jamais fédérée avec l'instance d'origine</li> + </ul> + <p>Solution : Malheureusement, il n'y a pas de solution simple pour ce problème, car il s'agit d'une limitation du réseau fédéré. Vous pouvez essayer de visiter manuellement les URLs qui ont échoué.</p> + + <h4>3.2 Erreurs d'authentification</h4> + <p>Si vous rencontrez des problèmes lors de l'authentification avec Misskey :</p> + <ul> + <li>Vérifiez que vous êtes bien connecté à votre compte Misskey</li> + <li>Assurez-vous que vous autorisez les cookies tiers dans votre navigateur</li> + <li>Essayez de vous déconnecter puis de vous reconnecter à votre compte Misskey</li> + </ul> + + <h4>3.3 Migration interrompue</h4> + <p>Si votre migration est interrompue (par exemple, en fermant l'onglet ou en perdant la connexion Internet), FavMasToKey peut la reprendre :</p> + <ul> + <li>Retournez simplement sur la page de FavMasToKey</li> + <li>L'application détectera automatiquement la migration en cours</li> + <li>Confirmez que vous souhaitez reprendre là où vous vous êtes arrêté</li> + </ul> + </div> + </div> + </div> + + <div class="card shadow-sm"> + <div class="card-body"> + <h2 class="card-title">Informations techniques</h2> + + <h3>Comment ça marche ?</h3> + <p>FavMasToKey fonctionne en suivant ces étapes :</p> + <ol> + <li><strong>Analyse du fichier JSON</strong> - L'application extrait les URLs des favoris depuis votre fichier Mastodon</li> + <li><strong>Authentification OAuth</strong> - L'application s'enregistre auprès de votre instance Misskey et obtient votre autorisation</li> + <li><strong>Recherche fédérée</strong> - Pour chaque favori, l'application recherche la publication équivalente sur le réseau fédéré</li> + <li><strong>Ajout aux favoris</strong> - Si la publication est trouvée, elle est ajoutée à vos favoris Misskey</li> + </ol> + + <h3>Confidentialité et sécurité</h3> + <p>FavMasToKey a été conçu en mettant l'accent sur la confidentialité et la sécurité :</p> + <ul> + <li>Aucune donnée n'est stockée sur le serveur, tout est traité localement dans votre navigateur</li> + <li>Les tokens d'authentification sont temporaires et ne sont stockés que pendant la durée de votre session</li> + <li>Le code est open source et peut être audité</li> + <li>L'application ne demande que les permissions minimales nécessaires (ajouter aux favoris)</li> + </ul> + + <h3>Limitations connues</h3> + <ul> + <li>Les publications qui n'existent plus ou qui sont privées ne peuvent pas être retrouvées</li> + <li>Les instances Misskey peuvent avoir des limites de taux (rate limits) qui ralentissent le processus</li> + <li>Les grandes collections de favoris peuvent prendre du temps à migrer</li> + </ul> + </div> + </div> + </div> + </div> + </div> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> +</body> +</html> \ No newline at end of file diff --git a/images/favicon.svg b/images/favicon.svg new file mode 100644 index 0000000..9316f56 --- /dev/null +++ b/images/favicon.svg @@ -0,0 +1,32 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" + width="500.000000pt" height="500.000000pt" viewBox="0 0 500.000000 500.000000" + preserveAspectRatio="xMidYMid meet"> + +<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)" +fill="#000000" stroke="none"> +<path d="M1999 4846 c-2 -2 -85 -9 -184 -15 -208 -14 -278 -23 -430 -58 -318 +-74 -569 -230 -763 -476 -148 -187 -206 -333 -247 -621 -26 -186 -27 -196 -21 +-560 3 -204 10 -441 15 -526 5 -85 14 -267 20 -404 6 -149 19 -299 31 -375 52 +-318 62 -380 76 -436 77 -311 211 -524 464 -740 112 -95 199 -151 362 -230 +168 -83 346 -136 586 -175 259 -42 468 -42 738 0 270 42 549 133 648 211 46 +36 56 75 56 217 0 136 -10 186 -40 202 -28 15 -88 12 -203 -10 -58 -11 -147 +-24 -198 -30 -52 -5 -141 -16 -199 -25 -146 -23 -398 -16 -530 14 -161 37 +-291 110 -351 198 -38 55 -89 192 -89 238 0 26 6 39 25 51 23 15 32 15 113 1 +357 -65 299 -61 795 -60 408 1 475 4 620 23 383 50 577 95 722 167 325 162 +543 453 601 803 29 170 36 336 41 910 4 544 3 568 -17 670 -55 276 -151 455 +-345 646 -201 198 -479 322 -790 352 -306 31 -596 43 -1041 42 -253 0 -463 -2 +-465 -4z m804 -824 c87 -30 116 -58 157 -152 18 -40 24 -74 25 -125 0 -100 +-30 -155 -150 -277 -52 -54 -95 -99 -95 -101 0 -1 44 -3 98 -2 89 0 101 -2 +144 -28 133 -80 271 -286 330 -493 32 -112 32 -314 -1 -428 -108 -383 -414 +-651 -783 -687 -242 -24 -426 20 -619 147 -172 114 -321 320 -374 519 -23 84 +-23 385 0 470 21 81 86 214 142 294 54 76 807 832 852 856 48 24 133 44 166 +39 17 -3 65 -17 108 -32z"/> +<path d="M2363 2992 c-106 -104 -190 -196 -204 -222 -20 -38 -24 -59 -24 -140 +0 -86 3 -100 29 -147 55 -99 158 -150 281 -141 120 10 206 71 243 171 45 119 +20 242 -69 344 -68 78 -97 170 -79 250 6 30 10 57 7 59 -2 3 -85 -76 -184 +-174z"/> +</g> +</svg> diff --git a/includes/app_data.php b/includes/app_data.php new file mode 100644 index 0000000..e69de29 diff --git a/includes/config.php b/includes/config.php index 679b10c..2345abd 100644 --- a/includes/config.php +++ b/includes/config.php @@ -22,14 +22,10 @@ if (ENVIRONMENT === 'development') { // Configuration de l'application $config = [ - // Informations de l'application (à remplir lors de la création de l'app sur Misskey) + // Informations de l'application 'app_name' => 'FavMasToKey', 'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey', - 'app_version' => '0.1.0', - - // Paramètres OAuth - À CONFIGURER - 'client_id' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey - 'client_secret' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey + 'app_version' => '0.2.0', // URLs de base 'app_url' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']), @@ -40,15 +36,43 @@ $config = [ // Paramètres pour le traitement 'batch_size' => 10, // Nombre de favoris à traiter en une fois 'timeout' => 30, // Timeout des requêtes en secondes - 'max_retries' => 3 // Nombre maximal de tentatives par favori + 'max_retries' => 3, // Nombre maximal de tentatives par favori + 'delay_between_requests' => 500 // Délai entre les requêtes en millisecondes (pour éviter le rate limiting) ]; +// Fichier de stockage des informations d'application par instance +$app_data_file = __DIR__ . '/app_data.php'; + +// Charger ou créer le fichier de données d'application +if (file_exists($app_data_file)) { + include $app_data_file; +} else { + // Structure initiale pour les données d'application + $app_data = [ + 'instances' => [] + ]; + + // Créer le fichier avec une structure protégée + $app_data_content = "<?php\n// Généré automatiquement - Ne pas modifier manuellement\nif (!defined('FAVMASTOKEY')) { die('Accès direct interdit'); }\n\$app_data = " . var_export($app_data, true) . ";\n?>"; + file_put_contents($app_data_file, $app_data_content); +} + +/** + * Sauvegarde les données d'application + */ +function save_app_data() { + global $app_data, $app_data_file; + + $app_data_content = "<?php\n// Généré automatiquement - Ne pas modifier manuellement\nif (!defined('FAVMASTOKEY')) { die('Accès direct interdit'); }\n\$app_data = " . var_export($app_data, true) . ";\n?>"; + file_put_contents($app_data_file, $app_data_content); +} + // Session session_start(); // Fonctions utilitaires function debug($data) { - if (ENVIRONMENT === 'development') { + if (ENVIRONMENT === 'production') { echo '<pre>'; print_r($data); echo '</pre>'; diff --git a/includes/functions.php b/includes/functions.php index 7182c02..7a83633 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -166,13 +166,221 @@ function misskey_api_request($instance, $endpoint, $data, $token) { } } +/** + * Enregistre une application sur l'instance Misskey + * + * @param string $instance Instance Misskey + * @return array Résultat de l'opération contenant client_id et client_secret + */ +function register_misskey_app($instance) { + global $config; + + // URL de l'API pour créer une application + $url = "https://{$instance}/api/app/create"; + + // Construire l'URL de callback + $callback_url = $config['app_url'] . '/callback.php'; + + // Données pour la création d'application + $data = [ + 'name' => $config['app_name'], + 'description' => $config['app_description'], + 'permission' => ['write:favorites'], + 'callbackUrl' => $callback_url + ]; + + // Initialiser cURL + $ch = curl_init(); + + // Configurer la requête + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($data), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'User-Agent: FavMasToKey/' . $config['app_version'] + ], + CURLOPT_TIMEOUT => $config['timeout'], + CURLOPT_SSL_VERIFYPEER => true + ]); + + // Exécuter la requête + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + // Fermer la session cURL + curl_close($ch); + + // Vérifier les erreurs + if ($error) { + return [ + 'success' => false, + 'message' => 'Erreur cURL: ' . $error, + 'http_code' => $http_code + ]; + } + + // Décoder la réponse + $response_data = json_decode($response, true); + + // Vérifier si la requête a réussi + if ($http_code >= 200 && $http_code < 300 && isset($response_data['id'])) { + return [ + 'success' => true, + 'client_id' => $response_data['id'], + 'client_secret' => $response_data['secret'], + 'http_code' => $http_code + ]; + } else { + return [ + 'success' => false, + 'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur API Misskey', + 'http_code' => $http_code, + 'data' => $response_data + ]; + } +} + +/** + * Échange un code d'autorisation contre un token d'accès + * + * @param string $instance Instance Misskey + * @param string $code Code d'autorisation reçu du serveur OAuth + * @param string $client_id ID de l'application + * @param string $client_secret Secret de l'application + * @return array Résultat de l'opération contenant le token d'accès + */ +function exchange_oauth_code($instance, $code, $client_id, $client_secret) { + global $config; + + // URL pour l'échange du code + $url = "https://{$instance}/oauth/token"; + + // Construire l'URL de callback (doit correspondre à celle utilisée pour l'autorisation) + $callback_url = $config['app_url'] . '/callback.php'; + + // Données pour l'échange du code + $data = [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $callback_url + ]; + + // Initialiser cURL + $ch = curl_init(); + + // Configurer la requête + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($data), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/x-www-form-urlencoded', + 'User-Agent: FavMasToKey/' . $config['app_version'] + ], + CURLOPT_TIMEOUT => $config['timeout'], + CURLOPT_SSL_VERIFYPEER => true + ]); + + // Exécuter la requête + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + // Fermer la session cURL + curl_close($ch); + + // Vérifier les erreurs + if ($error) { + return [ + 'success' => false, + 'message' => 'Erreur cURL: ' . $error, + 'http_code' => $http_code + ]; + } + + // Décoder la réponse + $response_data = json_decode($response, true); + + // Vérifier si la requête a réussi et si un token est présent + if ($http_code >= 200 && $http_code < 300 && isset($response_data['access_token'])) { + return [ + 'success' => true, + 'access_token' => $response_data['access_token'], + 'http_code' => $http_code + ]; + } else { + return [ + 'success' => false, + 'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur lors de l\'échange du code', + 'http_code' => $http_code, + 'data' => $response_data + ]; + } +} + +/** + * Recherche une note Mastodon sur le réseau fédéré de Misskey + * + * @param string $instance Instance Misskey + * @param string $url URL de la publication Mastodon + * @param string $token Token d'accès + * @return array Résultat de la recherche + */ +function search_federated_note($instance, $url, $token) { + // Endpoint de recherche + $endpoint = '/api/notes/search-by-url'; + + // Données pour la recherche + $data = [ + 'url' => $url + ]; + + // Effectuer la requête + $result = misskey_api_request($instance, $endpoint, $data, $token); + + return $result; +} + +/** + * Ajoute une note aux favoris sur Misskey + * + * @param string $instance Instance Misskey + * @param string $note_id ID de la note à ajouter aux favoris + * @param string $token Token d'accès + * @return array Résultat de l'opération + */ +function add_to_favorites($instance, $note_id, $token) { + global $config; + + // Endpoint pour ajouter aux favoris + $endpoint = $config['misskey_api_endpoint']; + + // Données pour l'ajout aux favoris + $data = [ + 'noteId' => $note_id + ]; + + // Effectuer la requête + $result = misskey_api_request($instance, $endpoint, $data, $token); + + return $result; +} + /** * Génère une URL d'autorisation OAuth pour Misskey * * @param string $instance Instance Misskey + * @param string $client_id ID de l'application * @return string URL d'autorisation */ -function generate_oauth_url($instance) { +function generate_oauth_url($instance, $client_id) { global $config; // Générer un état aléatoire pour la sécurité @@ -185,7 +393,7 @@ function generate_oauth_url($instance) { // Paramètres de la requête d'autorisation $params = [ - 'client_id' => $config['client_id'], + 'client_id' => $client_id, 'response_type' => 'code', 'redirect_uri' => $callback_url, 'scope' => 'write:favorites', diff --git a/index.php b/index.php index a7123be..9c064aa 100644 --- a/index.php +++ b/index.php @@ -22,10 +22,13 @@ if (isset($_SESSION['messages'])) { } ?> <!DOCTYPE html> -<html lang="fr"> +<html lang="fr" data-bs-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="theme-color" content="#7e57c2"> + <link rel="icon" href="images/favicon.svg" type="image/svg+xml"> + <meta name="description" content="FavMasToKey - Transférez vos favoris de Mastodon vers Misskey en quelques clics"> <title>FavMasToKey - Transférer vos favoris de Mastodon vers Misskey</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="css/styles.css"> @@ -35,6 +38,7 @@ if (isset($_SESSION['messages'])) { <header class="text-center mb-5"> <h1>FavMasToKey</h1> <p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p> + <p><a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a></p> </header> <!-- Messages d'alerte --> diff --git a/js/app.js b/js/app.js index ad9f085..a9d1d1c 100644 --- a/js/app.js +++ b/js/app.js @@ -26,6 +26,24 @@ document.addEventListener('DOMContentLoaded', function() { let totalItems = 0; let isProcessing = false; let isPaused = false; + let successCount = 0; + let errorCount = 0; + let skippedCount = 0; + let migration = { + status: 'not_started', // not_started, in_progress, paused, completed, error + startTime: null, + lastUpdateTime: null, + progress: { + current: 0, + total: 0, + percentage: 0 + }, + stats: { + success: 0, + error: 0, + skipped: 0 + } + }; // Gérer le téléchargement et l'analyse du fichier JSON if (uploadForm) { @@ -71,6 +89,26 @@ document.addEventListener('DOMContentLoaded', function() { // Stocker les données dans localStorage pour les conserver localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList)); + // Initialiser les données de migration + migration = { + status: 'not_started', + startTime: null, + lastUpdateTime: null, + progress: { + current: 0, + total: totalItems, + percentage: 0 + }, + stats: { + success: 0, + error: 0, + skipped: 0 + } + }; + + // Sauvegarder les données de migration + localStorage.setItem('favmastokey_migration', JSON.stringify(migration)); + } catch (error) { alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message); } @@ -120,17 +158,109 @@ document.addEventListener('DOMContentLoaded', function() { }); } + /** + * Met à jour les données de migration dans localStorage + */ + function updateMigrationData(status, progress = null) { + migration.status = status; + migration.lastUpdateTime = Date.now(); + + if (progress) { + migration.progress = progress; + } else { + migration.progress = { + current: currentIndex, + total: totalItems, + percentage: (currentIndex / totalItems) * 100 + }; + } + + migration.stats = { + success: successCount, + error: errorCount, + skipped: skippedCount + }; + + // Sauvegarder dans localStorage + localStorage.setItem('favmastokey_migration', JSON.stringify(migration)); + } + + /** + * Réinitialise les données de migration + */ + function resetMigration() { + currentIndex = 0; + successCount = 0; + errorCount = 0; + skippedCount = 0; + + migration = { + status: 'not_started', + startTime: null, + lastUpdateTime: null, + progress: { + current: 0, + total: totalItems, + percentage: 0 + }, + stats: { + success: 0, + error: 0, + skipped: 0 + } + }; + + localStorage.setItem('favmastokey_migration', JSON.stringify(migration)); + updateProgress(0); + } + // Gérer le processus de migration if (startMigration) { startMigration.addEventListener('click', function() { if (isProcessing) return; - // Récupérer les favoris depuis localStorage si nécessaire + // Récupérer les favoris et les données de migration depuis localStorage si nécessaire if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) { favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites')); totalItems = favoritesList.length; } + // Vérifier s'il y a une migration en cours à reprendre + if (localStorage.getItem('favmastokey_migration')) { + const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); + + // Si la migration était en cours ou en pause, proposer de la reprendre + if (savedMigration.status === 'in_progress' || savedMigration.status === 'paused') { + const resumeConfirm = confirm(`Une migration précédente a été trouvée (${savedMigration.progress.percentage.toFixed(1)}% terminée). Voulez-vous la reprendre?`); + + if (resumeConfirm) { + // Restaurer l'état de la migration + migration = savedMigration; + currentIndex = migration.progress.current; + + // Mettre à jour l'interface avec les données sauvegardées + updateProgress(migration.progress.percentage); + + // Restaurer les statistiques + successCount = migration.stats.success; + errorCount = migration.stats.error; + skippedCount = migration.stats.skipped; + + // Afficher un résumé + addLogEntry(`Reprise de la migration: ${successCount} réussis, ${errorCount} échecs, ${skippedCount} ignorés.`, 'info'); + } else { + // Réinitialiser la migration + resetMigration(); + } + } else { + // Réinitialiser la migration + resetMigration(); + } + } else { + // Initialiser une nouvelle migration + resetMigration(); + } + if (favoritesList.length === 0) { addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error'); return; @@ -142,6 +272,14 @@ document.addEventListener('DOMContentLoaded', function() { startMigration.classList.add('d-none'); pauseMigration.classList.remove('d-none'); + // Initialiser le temps de démarrage si c'est une nouvelle migration + if (migration.status === 'not_started' || migration.startTime === null) { + migration.startTime = Date.now(); + } + + // Mettre à jour le statut de la migration + updateMigrationData('in_progress'); + addLogEntry('Démarrage de la migration...', 'info'); // Lancer le processus de migration @@ -160,10 +298,17 @@ document.addEventListener('DOMContentLoaded', function() { pauseMigration.textContent = 'Reprendre'; addLogEntry('Migration en pause.', 'warning'); currentProgress.classList.remove('active'); + + // Mettre à jour le statut de la migration + updateMigrationData('paused'); } else { pauseMigration.textContent = 'Pause'; addLogEntry('Reprise de la migration...', 'info'); currentProgress.classList.add('active'); + + // Mettre à jour le statut de la migration + updateMigrationData('in_progress'); + processBatch(); } }); @@ -190,6 +335,9 @@ document.addEventListener('DOMContentLoaded', function() { pauseMigration.classList.add('d-none'); pauseMigration.textContent = 'Pause'; currentProgress.classList.remove('active'); + + // Réinitialiser les données de migration + resetMigration(); } }); } @@ -202,12 +350,15 @@ document.addEventListener('DOMContentLoaded', function() { if (currentIndex >= totalItems) { // Migration terminée isProcessing = false; - addLogEntry('Migration terminée avec succès !', 'success'); + const summary = `Migration terminée avec succès ! ${successCount} publications ajoutées aux favoris, ${errorCount} erreurs, ${skippedCount} déjà présentes.`; + addLogEntry(summary, 'success'); + startMigration.classList.remove('d-none'); startMigration.textContent = 'Terminer'; startMigration.addEventListener('click', function() { // Nettoyer localStorage et retourner à l'étape 1 localStorage.removeItem('favmastokey_favorites'); + localStorage.removeItem('favmastokey_migration'); step3.classList.add('d-none'); step1.classList.remove('d-none'); }); @@ -215,6 +366,9 @@ document.addEventListener('DOMContentLoaded', function() { // Mettre à jour la progression à 100% updateProgress(100); + + // Mettre à jour le statut de la migration + updateMigrationData('completed'); } return; } @@ -252,6 +406,15 @@ document.addEventListener('DOMContentLoaded', function() { if (data.results && data.results.length) { data.results.forEach(result => { addLogEntry(result.message, result.status); + + // Mettre à jour les compteurs + if (result.status === 'success') { + successCount++; + } else if (result.status === 'error') { + errorCount++; + } else if (result.status === 'info') { + skippedCount++; + } }); } @@ -259,7 +422,10 @@ document.addEventListener('DOMContentLoaded', function() { currentIndex = endIndex; // Mettre à jour la progression - updateProgress(); + updateProgress(data.progress.percentage); + + // Mettre à jour les données de migration + updateMigrationData('in_progress', data.progress); // Traiter le lot suivant après un court délai setTimeout(processBatch, 1000); @@ -271,6 +437,9 @@ document.addEventListener('DOMContentLoaded', function() { isPaused = true; pauseMigration.textContent = 'Reprendre'; currentProgress.classList.remove('active'); + + // Mettre à jour le statut de la migration + updateMigrationData('error'); } }) .catch(error => { diff --git a/oauth.php b/oauth.php index da6d5d0..d6a28d0 100644 --- a/oauth.php +++ b/oauth.php @@ -40,33 +40,48 @@ if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) { // Stocker l'instance dans la session $_SESSION['misskey_instance'] = $instance; -// En production, ici nous devrions vérifier si l'application est déjà enregistrée sur cette instance -// Si ce n'est pas le cas, il faudrait l'enregistrer via l'API Misskey -// Pour cette version initiale, nous utilisons des valeurs simulées +// Vérifier si cette instance est déjà enregistrée +$client_id = null; +$client_secret = null; + +if (isset($app_data['instances'][$instance])) { + $client_id = $app_data['instances'][$instance]['client_id']; + $client_secret = $app_data['instances'][$instance]['client_secret']; +} else { + // L'application n'est pas encore enregistrée, on l'enregistre + $registration = register_misskey_app($instance); + + if ($registration['success']) { + // Enregistrement réussi, stocker les informations + $client_id = $registration['client_id']; + $client_secret = $registration['client_secret']; + + // Sauvegarder dans le fichier de données + $app_data['instances'][$instance] = [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'registered_at' => time() + ]; + + save_app_data(); + } else { + // Échec de l'enregistrement + $_SESSION['messages'][] = [ + 'type' => 'danger', + 'text' => 'Erreur lors de l\'enregistrement de l\'application sur ' . $instance . ': ' . $registration['message'] + ]; + header('Location: index.php'); + exit; + } +} + +// Stocker le client_secret dans la session pour l'utiliser plus tard +$_SESSION['misskey_client_secret'] = $client_secret; // Générer l'URL d'autorisation try { - // REMARQUE: Pour une implémentation réelle, $config['client_id'] et $config['client_secret'] - // devraient être stockés par instance car chaque instance Misskey nécessite une application distincte - - // Générer un état aléatoire pour la sécurité - $state = bin2hex(random_bytes(16)); - $_SESSION['oauth_state'] = $state; - - // Construire l'URL de callback (doit correspondre à celle configurée dans l'application Misskey) - $callback_url = $config['app_url'] . '/callback.php'; - - // Paramètres de la requête d'autorisation - $params = [ - 'client_id' => $config['client_id'] ?: 'DEMO_CLIENT_ID', // En production, utiliser une valeur réelle - 'response_type' => 'code', - 'redirect_uri' => $callback_url, - 'scope' => 'write:favorites', - 'state' => $state - ]; - - // Construire l'URL d'autorisation - $auth_url = "https://{$instance}/oauth/authorize?" . http_build_query($params); + // Générer l'URL avec le client_id obtenu + $auth_url = generate_oauth_url($instance, $client_id); // Rediriger vers l'URL d'autorisation header('Location: ' . $auth_url); diff --git a/process.php b/process.php index c9f88ac..70386b8 100644 --- a/process.php +++ b/process.php @@ -65,61 +65,15 @@ foreach ($batch as $index => $url) { continue; } - // Extraire l'identifiant du toot - $pathParts = explode('/', trim($urlParts['path'], '/')); - $tootId = end($pathParts); + // Rechercher la note sur Misskey à partir de l'URL + $searchResult = search_federated_note($misskey_instance, $url, $token); - if (!is_numeric($tootId)) { - $results[] = [ - 'status' => 'error', - 'message' => "Impossible d'extraire l'ID du toot: $url" - ]; - continue; - } - - // Construire l'URL pour la recherche fédérée sur Misskey - $searchUrl = "https://" . $urlParts['host'] . "/@" . $pathParts[count($pathParts) - 3] . "/" . $tootId; - - // En production, ici nous ferions une recherche sur Misskey pour trouver l'équivalent du toot - // Pour cette version initiale, nous simulons la réussite/échec - - // Simulation : 90% de réussite, 10% d'échec - $success = (rand(1, 10) <= 9); - - if ($success) { - // Simulation de l'ajout aux favoris - $results[] = [ - 'status' => 'success', - 'message' => "Ajouté aux favoris: $url" - ]; - } else { - // Simulation d'erreur - $results[] = [ - 'status' => 'error', - 'message' => "Impossible d'ajouter aux favoris: $url (publication introuvable ou inaccessible)" - ]; - } - - // Dans un cas réel, nous aurions un code similaire à celui-ci: - /* - // Rechercher la note sur Misskey - $searchData = [ - 'query' => $searchUrl, - 'limit' => 1 - ]; - - $searchResult = misskey_api_request($misskey_instance, '/api/notes/search', $searchData, $token); - - if ($searchResult['success'] && !empty($searchResult['data'])) { - // Récupérer l'ID de la note trouvée - $noteId = $searchResult['data'][0]['id']; + if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) { + // Note trouvée, récupérer son ID + $noteId = $searchResult['data']['id']; - // Ajouter aux favoris - $favoriteData = [ - 'noteId' => $noteId - ]; - - $favoriteResult = misskey_api_request($misskey_instance, '/api/notes/favorites/create', $favoriteData, $token); + // Tenter d'ajouter la note aux favoris + $favoriteResult = add_to_favorites($misskey_instance, $noteId, $token); if ($favoriteResult['success']) { $results[] = [ @@ -127,21 +81,35 @@ foreach ($batch as $index => $url) { 'message' => "Ajouté aux favoris: $url" ]; } else { - $results[] = [ - 'status' => 'error', - 'message' => "Erreur lors de l'ajout aux favoris: " . $favoriteResult['message'] - ]; + // Vérifier si c'est une erreur de "déjà ajouté aux favoris" + $errorMessage = isset($favoriteResult['data']['error']['message']) + ? $favoriteResult['data']['error']['message'] + : $favoriteResult['message']; + + if (strpos($errorMessage, 'already') !== false) { + $results[] = [ + 'status' => 'info', + 'message' => "Déjà dans vos favoris: $url" + ]; + } else { + $results[] = [ + 'status' => 'error', + 'message' => "Erreur lors de l'ajout aux favoris: $errorMessage" + ]; + } } } else { + // Note non trouvée + $errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable"; + $results[] = [ 'status' => 'error', - 'message' => "Publication introuvable sur Misskey: $url" + 'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)" ]; } - */ - // Pause pour éviter de surcharger l'API (à utiliser en production) - // usleep(200000); // 200 ms + // Pause pour éviter le rate limiting + usleep($config['delay_between_requests'] * 1000); } // Renvoyer les résultats