Compare commits

...

4 Commits
0.4.1 ... main

Author SHA1 Message Date
3d5675c7a1 correction et amélioration de la page diagnostic 2025-03-22 14:33:25 +01:00
e39e76fd06 ajout du "mode filou"
permet de faire tourner différents jetons d'accès pour outrepasser les limitations vraiment beaucoup trop stricts de certaines api
2025-03-22 13:42:21 +01:00
d657ad0557 améliorations notables de la rapidité 2025-03-21 21:27:52 +01:00
87eeeafdde ajout du "mode tortue"
pour les instances vraiment trop stricts avec leur api ...
2025-03-21 20:20:12 +01:00
10 changed files with 3536 additions and 311 deletions

@ -173,10 +173,26 @@ $process_status = test_url($process_url);
if (isset($_SESSION['misskey_token'])) {
$token = $_SESSION['misskey_token'];
$masked_token = substr($token, 0, 4) . '...' . substr($token, -4);
echo '<code>' . htmlspecialchars($masked_token) . '</code>';
echo '<code>' . htmlspecialchars($masked_token) . '</code> (principal)';
} else {
echo '<span class="text-muted">Non défini</span>';
}
// Ajouter les tokens supplémentaires du mode filou
if (isset($_SESSION['additional_tokens']) && !empty($_SESSION['additional_tokens'])) {
echo '<br><strong>Tokens supplémentaires (mode filou):</strong><ul>';
foreach ($_SESSION['additional_tokens'] as $token_id => $token_data) {
$token = $token_data['token'];
$name = isset($token_data['name']) ? $token_data['name'] : 'Token sans nom';
$masked_token = substr($token, 0, 4) . '...' . substr($token, -4);
echo '<li><strong>' . htmlspecialchars($name) . ':</strong> <code>' .
htmlspecialchars($masked_token) . '</code></li>';
}
echo '</ul>';
}
?>
</dd>
@ -278,52 +294,6 @@ $process_status = test_url($process_url);
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Afficher les informations de localStorage
document.addEventListener('DOMContentLoaded', function() {
const localStorageInfo = document.getElementById('local-storage-info');
const clearLocalStorageBtn = document.getElementById('clear-localstorage');
function updateLocalStorageInfo() {
let html = '<ul class="list-unstyled mb-0">';
if (localStorage.getItem('favmastokey_favorites')) {
const favorites = JSON.parse(localStorage.getItem('favmastokey_favorites'));
html += '<li><strong>Favoris stockés:</strong> ' + favorites.length + ' URLs</li>';
} else {
html += '<li><strong>Favoris stockés:</strong> <span class="text-muted">Aucun</span></li>';
}
if (localStorage.getItem('favmastokey_migration')) {
const migration = JSON.parse(localStorage.getItem('favmastokey_migration'));
html += '<li><strong>État de la migration:</strong> ' + migration.status + '</li>';
html += '<li><strong>Progression:</strong> ' + migration.progress.current + '/' + migration.progress.total + ' (' + migration.progress.percentage.toFixed(1) + '%)</li>';
if (migration.lastUpdateTime) {
const date = new Date(migration.lastUpdateTime);
html += '<li><strong>Dernière mise à jour:</strong> ' + date.toLocaleString() + '</li>';
}
} else {
html += '<li><strong>État de la migration:</strong> <span class="text-muted">Aucune migration en cours</span></li>';
}
html += '</ul>';
localStorageInfo.innerHTML = html;
}
// Mettre à jour au chargement
updateLocalStorageInfo();
// Gérer le bouton d'effacement
clearLocalStorageBtn.addEventListener('click', function() {
if (confirm('Êtes-vous sûr de vouloir effacer toutes les données de migration stockées localement ?')) {
localStorage.removeItem('favmastokey_favorites');
localStorage.removeItem('favmastokey_migration');
updateLocalStorageInfo();
alert('Données localStorage effacées avec succès.');
}
});
});
</script>
<script src="js/diagnostic.js"></script>
</body>
</html>

@ -1,6 +1,6 @@
<?php
/**
* FavMasToKey - Configuration
* FavMasToKey - Configuration (version améliorée)
*/
// Empêcher l'accès direct au fichier
@ -29,7 +29,7 @@ $config = [
// Informations de l'application
'app_name' => 'FavMasToKey',
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
'app_version' => '0.4.1', // Mise à jour de la version pour le mode ultra-lent
'app_version' => '0.6.1', // Mise à jour de la version pour les améliorations
// URL de base - Utilisée pour les liens dans l'application
'app_url' => 'https://concepts.esenjin.xyz/favmastokey', // Remplacez par l'URL exacte de votre application
@ -41,12 +41,44 @@ $config = [
'batch_size' => 2,
'timeout' => 90,
'max_retries' => 3,
'delay_between_requests' => 3000, // Délai normal entre les requêtes (en millisecondes)
'slow_mode_delay' => 30000, // Délai par défaut en mode lent (30 secondes)
'slow_mode_min' => 10000, // Délai minimum pour le mode lent (10 secondes)
'slow_mode_max' => 60000, // Délai maximum pour le mode lent (60 secondes)
'adaptive_delay_step' => 5000, // Pas d'augmentation du délai en cas de rate limiting (5 secondes)
'adaptive_delay_max_increases' => 3 // Nombre maximum d'augmentations automatiques du délai
'delay_between_requests' => 3000, // Délai normal entre les requêtes (en millisecondes)
// Paramètres pour le mode lent
'slow_mode_delay' => 30000, // Délai par défaut en mode lent (30 secondes)
'slow_mode_min' => 10000, // Délai minimum pour le mode lent (10 secondes)
'slow_mode_max' => 300000, // Délai maximum pour le mode lent (5 minutes)
// Paramètres pour le mode tortue
'tortoise_mode_delay' => 120000, // Délai par défaut en mode tortue (2 minutes)
'tortoise_mode_min' => 60000, // Délai minimum pour le mode tortue (1 minute)
'tortoise_mode_max' => 300000, // Délai maximum pour le mode tortue (5 minutes)
// AMÉLIORATION 1: Paramètres pour la file d'attente des rate-limit
'rate_limit_queue_retry_delay' => 60000, // Délai avant de réessayer un élément en file d'attente (60 secondes)
'rate_limit_max_retries' => 5, // Nombre maximum de tentatives pour un même élément
// AMÉLIORATION 2: Paramètres pour l'optimisation des favoris déjà existants
'skip_delay_for_already_favorited' => true, // Ne pas attendre le délai pour les favoris déjà existants
// AMÉLIORATION 3: Paramètres pour le cache de fédération
'federation_cache_enabled' => true, // Activer le cache de fédération
'federation_cache_ttl' => 1440, // Durée de vie du cache en minutes (24 heures)
'federation_cache_cleanup_interval' => 60, // Intervalle de nettoyage du cache en minutes
// AMÉLIORATION 4: Paramètres pour l'adaptation des délais
'adaptive_delay_enabled' => true, // Activer l'ajustement automatique des délais
'adaptive_delay_step' => 5000, // Pas d'augmentation du délai en cas de rate limiting (5 secondes)
'adaptive_delay_max_increases' => 3, // Nombre maximum d'augmentations automatiques du délai
'adaptive_delay_decrease_rate' => 0.8, // Facteur de diminution du délai après succès (80%)
// AMÉLIORATION 5: Paramètres pour les points de sauvegarde
'checkpoint_interval' => 300000, // Intervalle entre les points de sauvegarde (5 minutes)
'checkpoint_max_stored' => 3, // Nombre maximum de points de sauvegarde à conserver
// Paramètres pour la pause automatique
'auto_pause_enabled' => true, // Activer la pause automatique en cas de trop de rate-limits
'auto_pause_duration' => 15, // Durée de pause automatique en minutes
'auto_pause_threshold' => 3, // Nombre d'erreurs de rate-limit consécutives avant pause
];
// Session
@ -81,4 +113,26 @@ function debug($data, $title = '', $log_to_file = false) {
echo '</pre>';
}
}
}
/**
* Journalise les performances des requêtes API pour analyse
*
* @param string $domain Domaine de l'instance
* @param string $action Type d'action (recherche, ajout favori, etc.)
* @param int $response_time Temps de réponse en millisecondes
* @param bool $success Succès de la requête
* @param string $error_type Type d'erreur si échec
*/
function log_api_performance($domain, $action, $response_time, $success, $error_type = null) {
if (ENVIRONMENT === 'development') {
$status = $success ? 'success' : ($error_type === 'rate_limit' ? 'rate_limit' : 'error');
$log_entry = date('Y-m-d H:i:s') . " | $domain | $action | {$response_time}ms | $status";
if ($error_type) {
$log_entry .= " | $error_type";
}
error_log("[API_PERF] " . $log_entry);
}
}

@ -1,6 +1,6 @@
<?php
/**
* FavMasToKey - Fonctions utilitaires
* FavMasToKey - Fonctions utilitaires (version améliorée)
*/
// Empêcher l'accès direct au fichier
@ -94,6 +94,17 @@ function extract_toot_ids($urls) {
return $ids;
}
/**
* Obtient l'URL de base d'une instance ActivityPub
*
* @param string $url URL complète
* @return string Domaine de l'instance
*/
function get_instance_domain($url) {
$parsed = parse_url($url);
return isset($parsed['host']) ? $parsed['host'] : '';
}
/**
* Effectue une requête cURL vers l'API Misskey
*
@ -101,11 +112,14 @@ function extract_toot_ids($urls) {
* @param string $endpoint Point d'accès API (ex: /api/notes/favorites/create)
* @param array $data Données à envoyer
* @param string $token Token d'accès OAuth
* @param bool $with_timing Indique si les informations de timing doivent être retournées
* @return array Résultat de la requête
*/
function misskey_api_request($instance, $endpoint, $data, $token) {
function misskey_api_request($instance, $endpoint, $data, $token, $with_timing = false) {
global $config;
$start_time = microtime(true);
// Construire l'URL complète
$url = "https://{$instance}{$endpoint}";
@ -134,17 +148,30 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
// Calcul du temps de réponse
$end_time = microtime(true);
$response_time = round(($end_time - $start_time) * 1000); // en millisecondes
// Fermer la session cURL
curl_close($ch);
// Vérifier les erreurs
if ($error) {
return [
$result = [
'success' => false,
'message' => 'Erreur cURL: ' . $error,
'http_code' => $http_code,
'error_code' => 'NETWORK_ERROR'
];
if ($with_timing) {
$result['timing'] = [
'response_time_ms' => $response_time,
'endpoint' => $endpoint
];
}
return $result;
}
// Décoder la réponse
@ -152,22 +179,40 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
// Analyse spécifique des erreurs
if ($http_code == 429) {
return [
$result = [
'success' => false,
'message' => 'Limite de taux (rate limit) dépassée. Réduisez la fréquence des requêtes ou activez le mode lent.',
'http_code' => $http_code,
'error_code' => 'RATE_LIMIT_EXCEEDED',
'data' => $response_data
];
if ($with_timing) {
$result['timing'] = [
'response_time_ms' => $response_time,
'endpoint' => $endpoint
];
}
return $result;
}
// Vérifier si la requête a réussi
if ($http_code >= 200 && $http_code < 300) {
return [
$result = [
'success' => true,
'data' => $response_data,
'http_code' => $http_code
];
if ($with_timing) {
$result['timing'] = [
'response_time_ms' => $response_time,
'endpoint' => $endpoint
];
}
return $result;
} else {
// Déterminer le type d'erreur
$error_code = 'API_ERROR';
@ -182,13 +227,22 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
}
}
return [
$result = [
'success' => false,
'message' => $error_message,
'http_code' => $http_code,
'error_code' => $error_code,
'data' => $response_data
];
if ($with_timing) {
$result['timing'] = [
'response_time_ms' => $response_time,
'endpoint' => $endpoint
];
}
return $result;
}
}
@ -243,6 +297,56 @@ function validate_misskey_token($instance, $token) {
];
}
/**
* Vérifie si une note est déjà dans les favoris
*
* @param string $instance Instance Misskey
* @param string $note_id ID de la note à vérifier
* @param string $token Token d'accès
* @return array Résultat de la vérification
*/
function check_if_favorited($instance, $note_id, $token) {
// Endpoint pour récupérer le statut de favori d'une note
$endpoint = '/api/notes/show';
// Données pour la requête
$data = [
'noteId' => $note_id
];
// Effectuer la requête avec timing pour les statistiques de performance
$result = misskey_api_request($instance, $endpoint, $data, $token, true);
if ($result['success'] && isset($result['data'])) {
// La note existe et nous avons une réponse
// Vérifier si l'utilisateur a déjà mis cette note en favori
if (isset($result['data']['isFavorited']) && $result['data']['isFavorited'] === true) {
return [
'success' => true,
'is_favorited' => true,
'message' => 'Cette note est déjà dans vos favoris',
'timing' => isset($result['timing']) ? $result['timing'] : null
];
} else {
return [
'success' => true,
'is_favorited' => false,
'message' => 'Cette note n\'est pas encore dans vos favoris',
'timing' => isset($result['timing']) ? $result['timing'] : null
];
}
}
// En cas d'erreur ou si la note n'existe pas
return [
'success' => false,
'is_favorited' => false,
'message' => isset($result['message']) ? $result['message'] : 'Impossible de vérifier le statut de favori',
'error_code' => isset($result['error_code']) ? $result['error_code'] : 'UNKNOWN_ERROR',
'timing' => isset($result['timing']) ? $result['timing'] : null
];
}
/**
* Recherche une note Mastodon sur le réseau fédéré de Misskey
* Utilise l'endpoint ap/show qui s'est avéré le plus fiable avec différentes instances
@ -265,8 +369,8 @@ function search_federated_note($instance, $url, $token) {
'uri' => $cleanUrl
];
// Effectuer la requête
$result = misskey_api_request($instance, $endpoint, $data, $token);
// Effectuer la requête avec timing pour les statistiques de performance
$result = misskey_api_request($instance, $endpoint, $data, $token, true);
// Journal pour le format de la réponse
if ($result['success'] && isset($result['data'])) {
@ -313,7 +417,7 @@ function search_federated_note($instance, $url, $token) {
}
// Méthode de secours 1: Essayer notes/search-by-url (parfois utilisé dans les anciennes versions)
$fallback_result = misskey_api_request($instance, '/api/notes/search-by-url', ['url' => $cleanUrl], $token);
$fallback_result = misskey_api_request($instance, '/api/notes/search-by-url', ['url' => $cleanUrl], $token, true);
if ($fallback_result['success'] && isset($fallback_result['data'])) {
error_log("Format de réponse search-by-url: " . json_encode(array_keys($fallback_result['data'])));
@ -354,7 +458,7 @@ function search_federated_note($instance, $url, $token) {
$remoteId = "https://{$acctDomain}/users/{$username}/statuses/{$statusId}";
// Essayer d'abord avec /api/notes/show
$remote_result = misskey_api_request($instance, '/api/notes/show', ['uri' => $remoteId], $token);
$remote_result = misskey_api_request($instance, '/api/notes/show', ['uri' => $remoteId], $token, true);
if ($remote_result['success'] && isset($remote_result['data']['id'])) {
return $remote_result;
@ -369,7 +473,7 @@ function search_federated_note($instance, $url, $token) {
$renote_result = misskey_api_request($instance, '/api/notes/search', [
'query' => "@{$username}@{$acctDomain} {$statusId}",
'limit' => 10
], $token);
], $token, true);
if ($renote_result['success'] && !empty($renote_result['data'])) {
// Parcourir les résultats pour trouver une correspondance
@ -394,7 +498,8 @@ function search_federated_note($instance, $url, $token) {
'success' => false,
'message' => "Impossible de trouver la publication sur le réseau fédéré après plusieurs tentatives",
'error_code' => 'NOT_FOUND',
'http_code' => isset($result['http_code']) ? $result['http_code'] : 404
'http_code' => isset($result['http_code']) ? $result['http_code'] : 404,
'timing' => isset($result['timing']) ? $result['timing'] : null
];
}
@ -417,8 +522,93 @@ function add_to_favorites($instance, $note_id, $token) {
'noteId' => $note_id
];
// Effectuer la requête avec timing pour les statistiques de performance
$result = misskey_api_request($instance, $endpoint, $data, $token, true);
return $result;
}
/**
* Récupère les favoris existants sur Misskey (pour une vérification en masse)
*
* @param string $instance Instance Misskey
* @param string $token Token d'accès
* @param int $limit Nombre de favoris à récupérer par page
* @param string $untilId ID pour la pagination
* @return array Liste des favoris et statut de la requête
*/
function get_existing_favorites($instance, $token, $limit = 100, $untilId = null) {
// Endpoint pour récupérer les favoris
$endpoint = '/api/i/favorites';
// Données pour la requête
$data = [
'limit' => $limit
];
// Ajouter l'ID pour la pagination si fourni
if ($untilId) {
$data['untilId'] = $untilId;
}
// Effectuer la requête
$result = misskey_api_request($instance, $endpoint, $data, $token);
return $result;
if ($result['success'] && isset($result['data']) && is_array($result['data'])) {
// Extraire les IDs
$favoriteIds = [];
foreach ($result['data'] as $favorite) {
if (isset($favorite['id'])) {
$favoriteIds[] = $favorite['id'];
}
}
// Déterminer s'il y a plus de résultats (pour la pagination)
$hasMore = count($result['data']) >= $limit;
$lastId = $hasMore && !empty($result['data']) ? end($result['data'])['id'] : null;
return [
'success' => true,
'favorites' => $favoriteIds,
'has_more' => $hasMore,
'last_id' => $lastId,
'count' => count($favoriteIds)
];
}
return [
'success' => false,
'message' => isset($result['message']) ? $result['message'] : 'Impossible de récupérer les favoris',
'error_code' => isset($result['error_code']) ? $result['error_code'] : 'UNKNOWN_ERROR'
];
}
/**
* Récupère les performances de l'API pour un domaine spécifique
* et recommande un délai adapté
*
* @param string $domain Domaine de l'instance
* @param array $stats Statistiques de performance
* @return int Délai recommandé en secondes
*/
function get_recommended_delay_for_domain($domain, $stats) {
// Valeurs par défaut
$baseDelay = 3; // secondes
$increment = 5; // secondes par échec de rate-limit
if (!$stats || !isset($stats['rateLimitCount'])) {
return $baseDelay;
}
// Calculer un délai basé sur le nombre d'erreurs de rate-limit
$rateLimitCount = (int)$stats['rateLimitCount'];
if ($rateLimitCount > 0) {
// Formule: délai de base + (nombre d'erreurs de rate-limit * incrément)
// Limité à un maximum de 300 secondes (5 minutes)
$recommendedDelay = min(300, $baseDelay + ($rateLimitCount * $increment));
return $recommendedDelay;
}
return $baseDelay;
}

@ -117,7 +117,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>
<p><a href="diagnostic.php" class="btn btn-sm btn-outline-primary">Diagnostic</a> <a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a> <a href="multitokens.php" class="btn btn-sm btn-outline-primary">Mode Filou</a></p>
</header>
<!-- Messages d'alerte -->
@ -235,9 +235,10 @@ if (isset($_SESSION['messages'])) {
</div>
<?php endif; ?>
<!-- Mode lent -->
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="slow-mode">
<label class="form-check-label" for="slow-mode">Mode ultra-lent</label>
<label class="form-check-label" for="slow-mode">Mode lent</label>
<div class="form-text">Activez cette option pour les instances Misskey avec des limitations d'API strictes</div>
</div>
@ -251,10 +252,36 @@ if (isset($_SESSION['messages'])) {
</div>
<div id="slow-mode-warning" class="alert alert-warning mb-4 d-none">
<strong>Mode ultra-lent activé</strong>
<p>Ce mode ajoute des délais très importants entre les requêtes (30 secondes par défaut) et ne traite qu'un seul favori à la fois. Cela rend l'importation beaucoup plus lente mais évite presque complètement le risque de blocage par l'API.</p>
<strong>Mode lent activé</strong>
<p>Ce mode ajoute des délais importants entre les requêtes (30 secondes par défaut) et ne traite qu'un seul favori à la fois. Cela rend l'importation plus lente mais permet d'éviter (dans une certaine mesure) le risque de blocage par l'API.</p>
<p class="mb-0">Recommandé si vous rencontrez systématiquement des erreurs de limite de taux (rate limit) ou si votre instance Misskey est extrêmement stricte sur le nombre de requêtes autorisées.</p>
</div>
<!-- Mode tortue -->
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="tortoise-mode">
<label class="form-check-label" for="tortoise-mode">Mode tortue</label>
<div class="form-text">Activez cette option pour les instances Misskey avec des limitations d'API extrêmement strictes</div>
</div>
<div id="tortoise-mode-options" class="mb-4 d-none">
<label for="tortoise-mode-delay" class="form-label">Délai entre les requêtes : <span id="tortoise-delay-value">120</span> secondes</label>
<input type="range" class="form-range" id="tortoise-mode-delay" min="60" max="300" step="30" value="120">
<div class="d-flex justify-content-between small text-muted">
<span>60s (plus rapide)</span>
<span>300s (5 min)</span>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="auto-pause-enabled" checked>
<label class="form-check-label" for="auto-pause-enabled">Pauses automatiques après détection de rate limit</label>
</div>
</div>
<div id="tortoise-mode-warning" class="alert alert-warning mb-4 d-none">
<strong>Mode tortue activé</strong>
<p>Ce mode utilise des délais extrêmement longs entre les requêtes (2-5 minutes par défaut) et effectue des pauses automatiques de 15 minutes lorsque des limitations d'API sont détectées.</p>
<p class="mb-0">Utilisez ce mode pour les instances Misskey avec des limites d'API très strictes. La migration sera <strong>extrêmement lente</strong> mais devrait pouvoir terminer sans interventions manuelles fréquentes.</p>
</div>
<div class="mb-4">
<label class="form-label">Progression globale</label>

1089
js/app.js

File diff suppressed because it is too large Load Diff

109
js/diagnostic.js Normal file

@ -0,0 +1,109 @@
// Gestion du localStorage pour la page de diagnostic
function initLocalStorageDiagnostic() {
const localStorageInfo = document.getElementById('local-storage-info');
const clearLocalStorageBtn = document.getElementById('clear-localstorage');
function updateLocalStorageInfo() {
let html = '<ul class="list-unstyled mb-0">';
// Vérifier les favoris
try {
const favoritesData = localStorage.getItem('favmastokey_favorites');
if (favoritesData) {
const favorites = JSON.parse(favoritesData);
html += '<li><strong>Favoris stockés:</strong> ' + (Array.isArray(favorites) ? favorites.length : '?') + ' URLs</li>';
} else {
html += '<li><strong>Favoris stockés:</strong> <span class="text-muted">Aucun</span></li>';
}
} catch (e) {
html += '<li><strong>Favoris stockés:</strong> <span class="text-danger">Erreur</span></li>';
}
// Vérifier la migration
try {
const migrationData = localStorage.getItem('favmastokey_migration');
if (migrationData) {
const migration = JSON.parse(migrationData);
html += '<li><strong>État de la migration:</strong> ' + (migration.status || 'Non défini') + '</li>';
if (migration.progress) {
html += '<li><strong>Progression:</strong> ' +
migration.progress.current + '/' +
migration.progress.total + ' (' +
(migration.progress.percentage || 0).toFixed(1) + '%)</li>';
}
if (migration.lastUpdateTime) {
const date = new Date(migration.lastUpdateTime);
html += '<li><strong>Dernière mise à jour:</strong> ' + date.toLocaleString() + '</li>';
}
} else {
html += '<li><strong>État de la migration:</strong> <span class="text-muted">Aucune migration en cours</span></li>';
}
} catch (e) {
html += '<li><strong>État de la migration:</strong> <span class="text-danger">Erreur</span></li>';
}
// Autres données
const additionalKeys = [
{key: 'favmastokey_federated_cache', label: 'Cache fédéré'},
{key: 'favmastokey_ratelimit_queue', label: 'File d\'attente rate-limit'},
{key: 'favmastokey_api_performance', label: 'Statistiques API'},
{key: 'favmastokey_multitoken_migration', label: 'Migration mode filou'}
];
for (const item of additionalKeys) {
try {
const dataStr = localStorage.getItem(item.key);
if (dataStr) {
const data = JSON.parse(dataStr);
const size = typeof data === 'object' ?
(Array.isArray(data) ? data.length : Object.keys(data).length) : 1;
html += `<li><strong>${item.label}:</strong> ${size} entrées</li>`;
}
} catch (e) {
// Ignorer les erreurs
}
}
html += '</ul>';
localStorageInfo.innerHTML = html;
}
// Mettre à jour au chargement
updateLocalStorageInfo();
// Gérer le bouton d'effacement
if (clearLocalStorageBtn) {
clearLocalStorageBtn.addEventListener('click', function() {
if (confirm('Êtes-vous sûr de vouloir effacer toutes les données de migration stockées localement ?')) {
// Liste des clés connues
const keysToRemove = [
'favmastokey_favorites',
'favmastokey_migration',
'favmastokey_federated_cache',
'favmastokey_ratelimit_queue',
'favmastokey_api_performance',
'favmastokey_multitoken_migration'
];
// Supprimer toutes les clés connues
keysToRemove.forEach(key => localStorage.removeItem(key));
// Rechercher d'autres clés potentielles
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('favmastokey_')) {
localStorage.removeItem(key);
}
}
updateLocalStorageInfo();
alert('Données localStorage effacées avec succès.');
}
});
}
}
// Exécuter après chargement du DOM
document.addEventListener('DOMContentLoaded', initLocalStorageDiagnostic);

995
js/multitokens.js Normal file

@ -0,0 +1,995 @@
/**
* FavMasToKey - Mode Filou (script de gestion des tokens multiples)
*/
// Attendre que le DOM soit chargé
document.addEventListener('DOMContentLoaded', function() {
// Éléments DOM
const uploadForm = document.getElementById('upload-form');
const jsonFileInput = document.getElementById('json-file');
const fileSummary = document.getElementById('file-summary');
const startMigration = document.getElementById('start-migration');
const pauseMigration = document.getElementById('pause-migration');
const cancelMigration = document.getElementById('cancel-migration');
const globalProgress = document.getElementById('global-progress');
const successProgress = document.getElementById('success-progress');
const skipProgress = document.getElementById('skip-progress');
const warnProgress = document.getElementById('warn-progress');
const errorProgress = document.getElementById('error-progress');
const operationLog = document.getElementById('operation-log');
const clearLog = document.getElementById('clear-log');
const statsDisplay = document.getElementById('stats-display');
// Sliders de configuration
const delayBetweenTokens = document.getElementById('delay-between-tokens');
const tokenCooldown = document.getElementById('token-cooldown');
const delayValue = document.getElementById('delay-value');
const cooldownValue = document.getElementById('cooldown-value');
const autoAdjust = document.getElementById('auto-adjust');
// Variables globales
let favoritesList = [];
let currentIndex = 0;
let totalItems = 0;
let isProcessing = false;
let isPaused = false;
let successCount = 0;
let errorCount = 0;
let skippedCount = 0;
let warningCount = 0;
// Variables pour la gestion des tokens
let tokens = [];
let tokensQueue = [];
let currentTokenIndex = 0;
let tokenTimers = {};
let activeRequests = 0;
let maxConcurrentRequests = 1; // Pour éviter les problèmes de race condition
// Options de migration
let migration = {
status: 'not_started',
startTime: null,
lastUpdateTime: null,
progress: {
current: 0,
total: 0,
percentage: 0
},
stats: {
success: 0,
error: 0,
skipped: 0,
warning: 0
},
options: {
delayBetweenTokens: 15, // secondes
tokenCooldown: 150, // secondes
autoAdjust: true
}
};
// ---- Gestionnaire d'événements ----
// Gérer les changements sur les sliders
if (delayBetweenTokens && delayValue) {
delayBetweenTokens.addEventListener('input', function() {
delayValue.textContent = this.value + 's';
migration.options.delayBetweenTokens = parseInt(this.value);
updateLocalStorage();
});
}
if (tokenCooldown && cooldownValue) {
tokenCooldown.addEventListener('input', function() {
cooldownValue.textContent = this.value + 's';
migration.options.tokenCooldown = parseInt(this.value);
updateLocalStorage();
});
}
if (autoAdjust) {
autoAdjust.addEventListener('change', function() {
migration.options.autoAdjust = this.checked;
updateLocalStorage();
});
}
// Gérer le téléchargement et l'analyse du fichier JSON
if (uploadForm) {
uploadForm.addEventListener('submit', function(e) {
e.preventDefault();
const file = jsonFileInput.files[0];
if (!file) {
alert('Veuillez sélectionner un fichier JSON.');
return;
}
// Vérifier l'extension du fichier
if (!file.name.endsWith('.json')) {
alert('Le fichier doit être au format JSON.');
return;
}
// Lire le fichier
const reader = new FileReader();
reader.onload = function(event) {
try {
const json = JSON.parse(event.target.result);
// Vérifier la structure du fichier
if (!json['@context'] || !json.type || !json.orderedItems) {
alert('Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.');
return;
}
favoritesList = json.orderedItems;
totalItems = favoritesList.length;
// Afficher un résumé
fileSummary.classList.remove('d-none');
fileSummary.innerHTML = `
<strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
Le transfert sera effectué avec les jetons configurés.
`;
// 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,
warning: 0
},
options: {
delayBetweenTokens: parseInt(delayBetweenTokens ? delayBetweenTokens.value : 15),
tokenCooldown: parseInt(tokenCooldown ? tokenCooldown.value : 150),
autoAdjust: autoAdjust ? autoAdjust.checked : true
}
};
// Sauvegarder les données de migration
updateLocalStorage();
// Scroll jusqu'à la section de migration
document.getElementById('step3').scrollIntoView({ behavior: 'smooth' });
} catch (error) {
alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message);
}
};
reader.onerror = function() {
alert('Erreur lors de la lecture du fichier.');
};
reader.readAsText(file);
});
}
// Effacer le journal des opérations
if (clearLog) {
clearLog.addEventListener('click', function() {
operationLog.innerHTML = '';
});
}
// Démarrer la migration
if (startMigration) {
startMigration.addEventListener('click', function() {
if (isProcessing) return;
// Récupérer les favoris du localStorage si nécessaire
if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) {
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
totalItems = favoritesList.length;
}
if (favoritesList.length === 0) {
addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error');
return;
}
// Vérifier s'il y a une migration en cours à reprendre
if (localStorage.getItem('favmastokey_multitoken_migration')) {
const savedMigration = JSON.parse(localStorage.getItem('favmastokey_multitoken_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;
// Restaurer les statistiques
successCount = migration.stats.success || 0;
errorCount = migration.stats.error || 0;
skippedCount = migration.stats.skipped || 0;
warningCount = migration.stats.warning || 0;
// Mettre à jour l'interface
updateProgress(migration.progress.percentage);
updateStatistics();
} else {
// Réinitialiser la migration
resetMigration();
}
} else {
// Réinitialiser la migration
resetMigration();
}
} else {
// Initialiser une nouvelle migration
resetMigration();
}
// Initialiser les tokens
initializeTokens();
if (tokens.length === 0) {
addLogEntry('Aucun jeton d\'accès disponible. Veuillez configurer au moins un jeton.', 'error');
return;
}
// Démarrer la migration
isProcessing = true;
isPaused = false;
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
migration.status = 'in_progress';
updateLocalStorage();
// Afficher un message de démarrage
const tokenCountMsg = tokens.length === 1 ? '1 jeton' : `${tokens.length} jetons`;
addLogEntry(`Démarrage de la migration avec ${tokenCountMsg}. Délai entre jetons: ${migration.options.delayBetweenTokens}s, Délai de récupération: ${migration.options.tokenCooldown}s`, 'info');
// Lancer le processus de migration
startTokenRotation();
});
}
// Gérer la pause de la migration
if (pauseMigration) {
pauseMigration.addEventListener('click', function() {
if (!isProcessing) return;
isPaused = !isPaused;
if (isPaused) {
pauseMigration.textContent = 'Reprendre';
addLogEntry('Migration en pause.', 'warning');
// Mettre à jour le statut de la migration
migration.status = 'paused';
updateLocalStorage();
} else {
pauseMigration.textContent = 'Pause';
addLogEntry('Reprise de la migration...', 'info');
// Mettre à jour le statut de la migration
migration.status = 'in_progress';
updateLocalStorage();
// Reprendre le traitement
startTokenRotation();
}
});
}
// Gérer l'annulation de la migration
if (cancelMigration) {
cancelMigration.addEventListener('click', function() {
if (!isProcessing && currentIndex === 0) return;
const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?');
if (confirmCancel) {
isProcessing = false;
isPaused = false;
addLogEntry('Migration annulée.', 'error');
// Nettoyer les timers
clearAllTimers();
// Réinitialiser l'interface
startMigration.classList.remove('d-none');
pauseMigration.classList.add('d-none');
pauseMigration.textContent = 'Pause';
// Réinitialiser les tokens
resetTokensStatus();
// Réinitialiser les données de migration
resetMigration();
}
});
}
// Vérifier si nous avons des données à restaurer au chargement
function initOnLoad() {
// Récupérer les favoris du localStorage
if (localStorage.getItem('favmastokey_favorites')) {
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
totalItems = favoritesList.length;
if (fileSummary) {
fileSummary.classList.remove('d-none');
fileSummary.innerHTML = `
<strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
Le transfert sera effectué avec les jetons configurés.
`;
}
}
// Récupérer les données de migration
if (localStorage.getItem('favmastokey_multitoken_migration')) {
migration = JSON.parse(localStorage.getItem('favmastokey_multitoken_migration'));
currentIndex = migration.progress.current;
successCount = migration.stats.success || 0;
errorCount = migration.stats.error || 0;
skippedCount = migration.stats.skipped || 0;
warningCount = migration.stats.warning || 0;
// Mettre à jour l'interface
updateProgress(migration.progress.percentage);
updateStatistics();
// Restaurer les options
if (migration.options) {
if (delayBetweenTokens && migration.options.delayBetweenTokens) {
delayBetweenTokens.value = migration.options.delayBetweenTokens;
if (delayValue) {
delayValue.textContent = migration.options.delayBetweenTokens + 's';
}
}
if (tokenCooldown && migration.options.tokenCooldown) {
tokenCooldown.value = migration.options.tokenCooldown;
if (cooldownValue) {
cooldownValue.textContent = migration.options.tokenCooldown + 's';
}
}
if (autoAdjust && migration.options.autoAdjust !== undefined) {
autoAdjust.checked = migration.options.autoAdjust;
}
}
// Si la migration était en cours, proposer de la reprendre
if ((migration.status === 'in_progress' || migration.status === 'paused') &&
migration.progress.current < migration.progress.total) {
const timeAgo = getTimeAgoString(new Date(migration.lastUpdateTime));
// Ajouter une entrée au journal
addLogEntry(`Une migration précédente a été trouvée (${migration.progress.percentage.toFixed(1)}% terminée, ${timeAgo}). Utilisez le bouton "Démarrer la migration" pour la reprendre.`, 'info');
}
}
}
// Initialiser les tokens à partir des éléments du DOM
function initializeTokens() {
tokens = [];
tokensQueue = [];
// Ajouter le token principal
tokens.push({
id: 'primary',
name: 'Token principal',
status: 'ready',
lastUsed: null,
cooldownUntil: null,
errorCount: 0,
element: document.querySelector('.token-card:first-child')
});
// Ajouter les tokens supplémentaires
const tokenCards = document.querySelectorAll('.token-card[data-token-id]');
tokenCards.forEach(card => {
const tokenId = card.getAttribute('data-token-id');
tokens.push({
id: tokenId,
name: card.querySelector('strong').textContent,
status: 'ready',
lastUsed: null,
cooldownUntil: null,
errorCount: 0,
element: card
});
});
// Initialiser la file d'attente avec tous les tokens
resetTokensQueue();
}
// Réinitialiser la file d'attente des tokens
function resetTokensQueue() {
tokensQueue = [...tokens]; // Copie de tous les tokens
// Mélanger pour éviter d'utiliser toujours le même ordre
shuffleArray(tokensQueue);
}
// Mélanger un tableau (algorithme de Fisher-Yates)
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// Démarrer la rotation des tokens
function startTokenRotation() {
if (!isProcessing || isPaused) {
return;
}
// Vérifier si nous avons terminé
if (currentIndex >= totalItems) {
finishMigration();
return;
}
// Si la file d'attente est vide, la réinitialiser
if (tokensQueue.length === 0) {
resetTokensQueue();
}
// Vérifier si on a un token disponible
const availableToken = findAvailableToken();
if (availableToken) {
// Traiter le prochain favori avec ce token
processWithToken(availableToken);
} else {
// Aucun token disponible actuellement, attendre et réessayer
addLogEntry('Tous les jetons sont en cours d\'utilisation ou en récupération. Attente...', 'info');
setTimeout(startTokenRotation, 5000);
}
}
// Trouver un token disponible
function findAvailableToken() {
const now = Date.now();
// Vérifier d'abord la file d'attente
for (let i = 0; i < tokensQueue.length; i++) {
const token = tokensQueue[i];
// Vérifier si le token est prêt
if (token.status === 'ready' &&
(!token.cooldownUntil || now >= token.cooldownUntil)) {
// Retirer ce token de la file d'attente
tokensQueue.splice(i, 1);
return token;
}
}
// Si aucun token n'est disponible dans la file d'attente,
// vérifier tous les tokens (au cas où un token aurait fini son cooldown)
for (const token of tokens) {
if (token.status === 'ready' &&
(!token.cooldownUntil || now >= token.cooldownUntil)) {
// Retirer ce token de la file d'attente s'il y est encore
const queueIndex = tokensQueue.findIndex(t => t.id === token.id);
if (queueIndex !== -1) {
tokensQueue.splice(queueIndex, 1);
}
return token;
}
}
// Aucun token disponible
return null;
}
// Traiter un favori avec un token spécifique
function processWithToken(token) {
if (!isProcessing || isPaused) {
return;
}
// Mettre à jour le statut du token
updateTokenStatus(token, 'active');
// URL à traiter
const url = favoritesList[currentIndex];
// Ajouter une entrée dans le journal
addLogEntry(`[${token.name}] Traitement de: ${getTruncatedUrl(url)}`, 'info');
// Envoyer la requête au serveur
fetch('process_multitoken.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: url,
token_id: token.id,
current_index: currentIndex,
total_items: totalItems
})
})
.then(response => response.json())
.then(data => {
// Mettre à jour les statistiques en fonction du résultat
handleProcessResult(data, token, url);
// Mettre à jour l'index
currentIndex++;
// Mettre à jour la progression
updateProgress();
// Mettre le token en cooldown
const cooldownPeriod = migration.options.tokenCooldown * 1000; // Convertir en millisecondes
const cooldownUntil = Date.now() + cooldownPeriod;
token.lastUsed = Date.now();
token.cooldownUntil = cooldownUntil;
updateTokenStatus(token, 'cooldown');
// Programmer la réactivation du token après le cooldown
tokenTimers[token.id] = setTimeout(() => {
// Remettre le token en état "prêt"
updateTokenStatus(token, 'ready');
// Ajouter le token à la fin de la file d'attente
tokensQueue.push(token);
}, cooldownPeriod);
// Démarrer le traitement du prochain favori après un délai
setTimeout(startTokenRotation, migration.options.delayBetweenTokens * 1000);
})
.catch(error => {
console.error('Erreur lors du traitement:', error);
// Ajouter l'erreur au journal
addLogEntry(`[${token.name}] Erreur lors du traitement: ${error.message}`, 'error');
// Incrémenter le compteur d'erreurs du token
token.errorCount = (token.errorCount || 0) + 1;
// Si trop d'erreurs avec ce token, le mettre en erreur
if (token.errorCount >= 3) {
updateTokenStatus(token, 'error');
addLogEntry(`[${token.name}] Ce jeton rencontre des problèmes répétés et a été désactivé.`, 'error');
} else {
// Sinon, mettre le token en cooldown plus court
const cooldownPeriod = 30 * 1000; // 30 secondes
const cooldownUntil = Date.now() + cooldownPeriod;
token.lastUsed = Date.now();
token.cooldownUntil = cooldownUntil;
updateTokenStatus(token, 'cooldown');
// Programmer la réactivation du token après le cooldown
tokenTimers[token.id] = setTimeout(() => {
updateTokenStatus(token, 'ready');
tokensQueue.push(token);
}, cooldownPeriod);
}
// Augmenter le compteur d'erreurs
errorCount++;
updateStatistics();
// Continuer avec le prochain token après un délai
setTimeout(startTokenRotation, migration.options.delayBetweenTokens * 1000);
});
}
// Gérer le résultat du traitement
function handleProcessResult(data, token, url) {
if (data.success) {
// Résultat du traitement
const result = data.result;
// Construction du message avec détails si disponibles
let message = result.message;
if (result.details) {
message += ` (${result.details})`;
}
// Ajouter l'entrée au journal avec le nom du token
addLogEntry(`[${token.name}] ${message}`, result.status);
// Mettre à jour les compteurs
if (result.status === 'success') {
successCount++;
token.errorCount = 0; // Réinitialiser le compteur d'erreurs
} else if (result.status === 'error') {
errorCount++;
// Incrémenter le compteur d'erreurs du token
token.errorCount = (token.errorCount || 0) + 1;
// Si autoAdjust est activé et qu'il y a beaucoup d'erreurs, augmenter les délais
if (migration.options.autoAdjust && token.errorCount >= 2) {
if (result.error_type === 'rate_limit') {
// Augmenter le délai entre les tokens
if (migration.options.delayBetweenTokens < 60) {
migration.options.delayBetweenTokens += 5;
if (delayBetweenTokens) delayBetweenTokens.value = migration.options.delayBetweenTokens;
if (delayValue) delayValue.textContent = migration.options.delayBetweenTokens + 's';
addLogEntry(`Délai entre jetons automatiquement augmenté à ${migration.options.delayBetweenTokens}s en raison de limitations d'API.`, 'warning');
}
// Augmenter le délai de cooldown
if (migration.options.tokenCooldown < 300) {
migration.options.tokenCooldown += 30;
if (tokenCooldown) tokenCooldown.value = migration.options.tokenCooldown;
if (cooldownValue) cooldownValue.textContent = migration.options.tokenCooldown + 's';
addLogEntry(`Délai de récupération automatiquement augmenté à ${migration.options.tokenCooldown}s.`, 'warning');
}
// Mettre à jour le localStorage
updateLocalStorage();
}
}
} else if (result.status === 'info') {
skippedCount++;
token.errorCount = 0; // Réinitialiser le compteur d'erreurs
} else if (result.status === 'warning') {
warningCount++;
// Si c'est une erreur de rate limit, incrémenter le compteur d'erreurs
if (result.error_type === 'rate_limit') {
token.errorCount = (token.errorCount || 0) + 1;
}
}
// Mettre à jour les statistiques
updateStatistics();
} else {
// Erreur lors du traitement
addLogEntry(`[${token.name}] Erreur: ${data.message}`, 'error');
errorCount++;
updateStatistics();
// Incrémenter le compteur d'erreurs du token
token.errorCount = (token.errorCount || 0) + 1;
}
}
// Mettre à jour le statut d'un token
function updateTokenStatus(token, status) {
token.status = status;
if (token.element) {
// Mettre à jour la classe CSS
token.element.classList.remove('active', 'cooldown', 'error');
// Mettre à jour l'indicateur de statut
const statusIndicator = token.element.querySelector('.token-status');
const statusText = token.element.querySelector('.text-muted.small');
if (statusIndicator) {
statusIndicator.classList.remove('ready', 'cooldown', 'error', 'idle');
}
switch (status) {
case 'active':
token.element.classList.add('active');
if (statusIndicator) statusIndicator.classList.add('ready');
if (statusText) statusText.textContent = 'En cours d\'utilisation';
break;
case 'cooldown':
token.element.classList.add('cooldown');
if (statusIndicator) statusIndicator.classList.add('cooldown');
if (statusText && token.cooldownUntil) {
const cooldownSeconds = Math.ceil((token.cooldownUntil - Date.now()) / 1000);
statusText.textContent = `Récupération: ${cooldownSeconds}s`;
// Mettre à jour le compteur de cooldown toutes les secondes
const updateInterval = setInterval(() => {
const remainingSeconds = Math.ceil((token.cooldownUntil - Date.now()) / 1000);
if (remainingSeconds <= 0) {
clearInterval(updateInterval);
if (statusText) statusText.textContent = 'Prêt';
} else {
if (statusText) statusText.textContent = `Récupération: ${remainingSeconds}s`;
}
}, 1000);
}
break;
case 'error':
token.element.classList.add('error');
if (statusIndicator) statusIndicator.classList.add('error');
if (statusText) statusText.textContent = 'Erreur';
break;
case 'ready':
default:
if (statusIndicator) statusIndicator.classList.add('ready');
if (statusText) statusText.textContent = 'Prêt';
break;
}
}
}
// Réinitialiser le statut de tous les tokens
function resetTokensStatus() {
tokens.forEach(token => {
updateTokenStatus(token, 'ready');
token.lastUsed = null;
token.cooldownUntil = null;
token.errorCount = 0;
});
}
// Nettoyer tous les timers
function clearAllTimers() {
Object.keys(tokenTimers).forEach(tokenId => {
clearTimeout(tokenTimers[tokenId]);
delete tokenTimers[tokenId];
});
}
// Obtenir une version tronquée d'une URL pour l'affichage
function getTruncatedUrl(url) {
if (url.length <= 60) return url;
const urlObj = new URL(url);
const domain = urlObj.hostname;
const path = urlObj.pathname;
const lastSegment = path.split('/').pop();
return `${domain}/.../${lastSegment}`;
}
// Générer une chaîne "il y a X minutes/heures" à partir d'une date
function getTimeAgoString(date) {
const now = new Date();
const diffMs = now - date;
const diffSeconds = Math.floor(diffMs / 1000);
if (diffSeconds < 60) {
return `il y a ${diffSeconds} seconde${diffSeconds > 1 ? 's' : ''}`;
}
const diffMinutes = Math.floor(diffSeconds / 60);
if (diffMinutes < 60) {
return `il y a ${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}`;
}
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) {
return `il y a ${diffHours} heure${diffHours > 1 ? 's' : ''}`;
}
const diffDays = Math.floor(diffHours / 24);
return `il y a ${diffDays} jour${diffDays > 1 ? 's' : ''}`;
}
/**
* Réinitialise les données de migration
*/
function resetMigration() {
currentIndex = 0;
successCount = 0;
errorCount = 0;
skippedCount = 0;
warningCount = 0;
migration = {
status: 'not_started',
startTime: null,
lastUpdateTime: null,
progress: {
current: 0,
total: totalItems,
percentage: 0
},
stats: {
success: 0,
error: 0,
skipped: 0,
warning: 0
},
options: {
delayBetweenTokens: parseInt(delayBetweenTokens ? delayBetweenTokens.value : 15),
tokenCooldown: parseInt(tokenCooldown ? tokenCooldown.value : 150),
autoAdjust: autoAdjust ? autoAdjust.checked : true
}
};
// Mettre à jour le localStorage
updateLocalStorage();
// Mettre à jour l'interface
updateProgress(0);
updateStatistics();
}
/**
* Met à jour les données de migration dans localStorage
*/
function updateLocalStorage() {
migration.lastUpdateTime = Date.now();
migration.progress = {
current: currentIndex,
total: totalItems,
percentage: (currentIndex / totalItems) * 100
};
migration.stats = {
success: successCount,
error: errorCount,
skipped: skippedCount,
warning: warningCount
};
if (delayBetweenTokens && tokenCooldown && autoAdjust) {
migration.options = {
delayBetweenTokens: parseInt(delayBetweenTokens.value),
tokenCooldown: parseInt(tokenCooldown.value),
autoAdjust: autoAdjust.checked
};
}
// Sauvegarder dans localStorage
localStorage.setItem('favmastokey_multitoken_migration', JSON.stringify(migration));
}
/**
* Terminer la migration avec succès
*/
function finishMigration() {
isProcessing = false;
// Nettoyer les timers
clearAllTimers();
const summary = `Migration terminée ! ${successCount} publications ajoutées aux favoris, ${errorCount} échecs, ${skippedCount} déjà présentes, ${warningCount} avertissements.`;
addLogEntry(summary, 'success');
startMigration.classList.remove('d-none');
startMigration.textContent = 'Terminer';
startMigration.addEventListener('click', function onceFinished() {
// Nettoyer localStorage
localStorage.removeItem('favmastokey_multitoken_migration');
// Ne pas supprimer les favoris pour permettre une nouvelle migration si nécessaire
// localStorage.removeItem('favmastokey_favorites');
// Réinitialiser l'interface
resetTokensStatus();
startMigration.textContent = 'Démarrer la migration';
// Retirer cet écouteur d'événement pour éviter les doublons
startMigration.removeEventListener('click', onceFinished);
// Scroll vers le haut
window.scrollTo({ top: 0, behavior: 'smooth' });
});
pauseMigration.classList.add('d-none');
// Mettre à jour la progression à 100%
updateProgress(100);
// Mettre à jour le statut de la migration
migration.status = 'completed';
updateLocalStorage();
}
/**
* Met à jour la barre de progression
*/
function updateProgress(forcedValue = null) {
const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100;
if (globalProgress) {
globalProgress.style.width = progress + '%';
globalProgress.textContent = Math.round(progress) + '%';
globalProgress.setAttribute('aria-valuenow', progress);
}
// Mettre à jour le localStorage
updateLocalStorage();
}
/**
* Met à jour les statistiques de progression
*/
function updateStatistics() {
// Calculer les pourcentages pour les barres de progression
const total = successCount + errorCount + skippedCount + warningCount;
const successPct = total > 0 ? (successCount / total) * 100 : 0;
const skipPct = total > 0 ? (skippedCount / total) * 100 : 0;
const warnPct = total > 0 ? (warningCount / total) * 100 : 0;
const errorPct = total > 0 ? (errorCount / total) * 100 : 0;
// Mettre à jour les barres
if (successProgress) {
successProgress.style.width = successPct + '%';
successProgress.setAttribute('aria-valuenow', successPct);
}
if (skipProgress) {
skipProgress.style.width = skipPct + '%';
skipProgress.setAttribute('aria-valuenow', skipPct);
}
if (warnProgress) {
warnProgress.style.width = warnPct + '%';
warnProgress.setAttribute('aria-valuenow', warnPct);
}
if (errorProgress) {
errorProgress.style.width = errorPct + '%';
errorProgress.setAttribute('aria-valuenow', errorPct);
}
// Mettre à jour le texte des statistiques
if (statsDisplay) {
statsDisplay.textContent = `${successCount} succès, ${skippedCount} ignorés, ${warningCount} avertissements, ${errorCount} erreurs`;
}
// Mettre à jour le localStorage
updateLocalStorage();
}
/**
* Ajoute une entrée dans le journal des opérations
*/
function addLogEntry(message, status = 'info') {
if (!operationLog) return;
const entry = document.createElement('div');
entry.className = `log-entry ${status}`;
const timestamp = new Date().toLocaleTimeString();
entry.textContent = `[${timestamp}] ${message}`;
operationLog.appendChild(entry);
// Scroll vers le bas pour voir la dernière entrée
const logContainer = document.getElementById('log-container');
if (logContainer) {
logContainer.scrollTop = logContainer.scrollHeight;
}
}
// Initialiser la page au chargement
initOnLoad();
});

570
multitokens.php Normal file

@ -0,0 +1,570 @@
<?php
/**
* FavMasToKey - Mode Filou (multiple tokens)
*/
// Définir la constante pour inclure les fichiers
define('FAVMASTOKEY', true);
// Inclure les fichiers requis
require_once 'includes/config.php';
require_once 'includes/functions.php';
// Vérifier si l'utilisateur est authentifié (au moins avec un token)
$has_primary_token = isset($_SESSION['misskey_token']) && !empty($_SESSION['misskey_token']);
$instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
// Traiter le formulaire de connexion Misskey (pour le token principal)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'connect_token') {
if (isset($_POST['misskey_instance']) && isset($_POST['misskey_token'])) {
$instance = trim($_POST['misskey_instance']);
$token = trim($_POST['misskey_token']);
// Vérifier que l'instance et le token sont valides
if (empty($instance) || empty($token)) {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Veuillez renseigner à la fois l\'instance Misskey et le jeton d\'accès.'
];
} else {
// Valider le format de l'instance
$instance = preg_replace('/^https?:\/\//', '', $instance);
$instance = rtrim($instance, '/');
if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'L\'URL de l\'instance Misskey semble invalide.'
];
} else {
// Vérifier la validité du token en effectuant une requête test
$validate_result = validate_misskey_token($instance, $token);
if ($validate_result['success']) {
// Stocker le token et l'instance dans la session
$_SESSION['misskey_token'] = $token;
$_SESSION['misskey_instance'] = $instance;
// Initialiser le tableau des tokens supplémentaires s'il n'existe pas
if (!isset($_SESSION['additional_tokens'])) {
$_SESSION['additional_tokens'] = [];
}
$_SESSION['messages'][] = [
'type' => 'success',
'text' => 'Connecté avec succès à ' . $instance . ' (token principal).'
];
// Rediriger vers la section des tokens multiples
header('Location: multitokens.php#tokens');
exit;
} else {
// Formater le message d'erreur correctement
$errorDetails = isset($validate_result['message']) ? $validate_result['message'] : 'Erreur inconnue';
// Si le message est un tableau, le convertir en chaîne JSON
if (is_array($errorDetails)) {
$errorDetails = json_encode($errorDetails, JSON_PRETTY_PRINT);
}
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Le jeton d\'accès semble invalide ou a expiré.',
'details' => $errorDetails
];
}
}
}
} else {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Données manquantes.'
];
}
}
// Traiter l'ajout d'un token supplémentaire
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_token') {
if (isset($_POST['token_name']) && isset($_POST['token_value'])) {
$name = trim($_POST['token_name']);
$token = trim($_POST['token_value']);
// Vérifier que le nom et le token sont valides
if (empty($name) || empty($token)) {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Veuillez renseigner à la fois un nom et un jeton d\'accès.'
];
} else {
// Vérifier la validité du token en effectuant une requête test
$validate_result = validate_misskey_token($instance, $token);
if ($validate_result['success']) {
// Ajouter le token au tableau des tokens supplémentaires
if (!isset($_SESSION['additional_tokens'])) {
$_SESSION['additional_tokens'] = [];
}
// Générer un ID unique pour ce token
$token_id = uniqid('token_');
$_SESSION['additional_tokens'][$token_id] = [
'name' => $name,
'token' => $token,
'added_at' => time(),
'last_used' => null,
'usage_count' => 0
];
$_SESSION['messages'][] = [
'type' => 'success',
'text' => 'Token supplémentaire "' . $name . '" ajouté avec succès.'
];
// Rediriger pour éviter la soumission multiple
header('Location: multitokens.php#tokens');
exit;
} else {
$errorDetails = isset($validate_result['message']) ? $validate_result['message'] : 'Erreur inconnue';
if (is_array($errorDetails)) {
$errorDetails = json_encode($errorDetails, JSON_PRETTY_PRINT);
}
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Le jeton d\'accès supplémentaire "' . $name . '" semble invalide ou a expiré.',
'details' => $errorDetails
];
}
}
} else {
$_SESSION['messages'][] = [
'type' => 'danger',
'text' => 'Données manquantes pour l\'ajout du token.'
];
}
}
// Traiter la suppression d'un token
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'remove_token') {
if (isset($_POST['token_id']) && isset($_SESSION['additional_tokens'][$_POST['token_id']])) {
$token_id = $_POST['token_id'];
$token_name = $_SESSION['additional_tokens'][$token_id]['name'];
// Supprimer le token
unset($_SESSION['additional_tokens'][$token_id]);
$_SESSION['messages'][] = [
'type' => 'info',
'text' => 'Token "' . $token_name . '" supprimé avec succès.'
];
// Rediriger pour éviter la soumission multiple
header('Location: multitokens.php#tokens');
exit;
}
}
// Traitement de la déconnexion complète
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
// Supprimer les informations d'authentification
unset($_SESSION['misskey_token']);
unset($_SESSION['misskey_instance']);
unset($_SESSION['additional_tokens']);
$_SESSION['messages'][] = [
'type' => 'info',
'text' => 'Vous avez été déconnecté (tous les tokens ont été supprimés).'
];
// Rediriger vers la page d'accueil
header('Location: multitokens.php');
exit;
}
// Initialiser les messages
$messages = [];
if (isset($_SESSION['messages'])) {
$messages = $_SESSION['messages'];
unset($_SESSION['messages']);
}
// Récupérer le nombre de tokens supplémentaires
$additional_tokens_count = isset($_SESSION['additional_tokens']) ? count($_SESSION['additional_tokens']) : 0;
$total_tokens_count = $has_primary_token ? 1 + $additional_tokens_count : $additional_tokens_count;
// Récupérer les favoris du localStorage
$has_favorites = false;
?>
<!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 Mode Filou - Accélérez le transfert de vos favoris de Mastodon vers Misskey avec plusieurs jetons">
<title>Mode Filou - 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">
<style>
.token-card {
transition: all 0.3s ease;
border-left: 4px solid #7e57c2;
}
.token-card.active {
border-left-color: #4caf50;
background-color: rgba(76, 175, 80, 0.1);
}
.token-card.cooldown {
border-left-color: #ffb74d;
background-color: rgba(255, 183, 77, 0.1);
}
.token-card.error {
border-left-color: #f44336;
background-color: rgba(244, 67, 54, 0.1);
}
.token-status {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
.token-status.ready {
background-color: #4caf50;
}
.token-status.cooldown {
background-color: #ffb74d;
}
.token-status.error {
background-color: #f44336;
}
.token-status.idle {
background-color: #9e9e9e;
}
#tokens-container {
max-height: 400px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="container py-5">
<header class="text-center mb-5">
<h1>FavMasToKey - Mode Filou</h1>
<p class="lead">Accélérez le transfert de vos favoris avec plusieurs jetons d'accès</p>
<div class="mt-3">
<a href="index.php" class="btn btn-sm btn-outline-primary">Mode Standard</a>
<a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a>
<a href="diagnostic.php" class="btn btn-sm btn-outline-primary">Diagnostic</a>
</div>
</header>
<!-- Messages d'alerte -->
<?php if (!empty($messages)): ?>
<?php foreach ($messages as $message): ?>
<div class="alert alert-<?php echo $message['type']; ?> alert-dismissible fade show" role="alert">
<?php echo $message['text']; ?>
<?php if (isset($message['details']) && ENVIRONMENT === 'development'): ?>
<hr>
<details>
<summary>Détails techniques (mode développement)</summary>
<pre class="mt-2 p-2 bg-dark text-light"><?php echo htmlspecialchars($message['details']); ?></pre>
</details>
<?php endif; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button>
</div>
<?php endforeach; ?>
<?php endif; ?>
<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">Comment ça marche ?</h2>
<p>Le <strong>Mode Filou</strong> permet d'accélérer le transfert de vos favoris en utilisant plusieurs jetons d'accès en rotation. Cela permet de contourner les limites d'API de certaines instances Misskey très strictes.</p>
<div class="alert alert-info">
<h5><i class="bi bi-info-circle"></i> Principe de fonctionnement</h5>
<p>Au lieu d'utiliser un seul jeton avec un long délai entre chaque requête, le mode filou va :</p>
<ol>
<li>Utiliser plusieurs jetons en rotation</li>
<li>Distribuer les requêtes entre ces jetons</li>
<li>Respecter un délai suffisant avant de réutiliser un même jeton</li>
</ol>
<p class="mb-0">Par exemple, avec 10 jetons et un délai de 15 secondes entre chaque requête, vous pourrez traiter un favori toutes les 15 secondes tout en n'utilisant chaque jeton qu'une fois toutes les 150 secondes.</p>
</div>
<div class="alert alert-warning">
<h5><i class="bi bi-exclamation-triangle"></i> Utilisation responsable</h5>
<p>Bien que cette méthode soit efficace, veuillez l'utiliser de manière responsable :</p>
<ul>
<li>Utilisez uniquement vos propres jetons (n'empruntez pas les jetons d'autres personnes)</li>
<li>Respectez un délai raisonnable entre les requêtes pour ne pas surcharger l'instance</li>
<li>Limitez le nombre de jetons à ce qui est nécessaire pour votre migration</li>
</ul>
</div>
</div>
</div>
<!-- Étape 1: Téléchargement du fichier JSON -->
<div class="card shadow-sm mb-4" id="step1">
<div class="card-header text-white">
<h3 class="card-title h5 mb-0">1. Importer vos favoris Mastodon</h3>
</div>
<div class="card-body">
<p>Téléchargez d'abord votre fichier d'export de favoris depuis Mastodon.</p>
<div class="card bg-light mb-3">
<div class="card-body">
<h5>Comment obtenir mon fichier de favoris ?</h5>
<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>
</div>
</div>
<form id="upload-form" class="mt-4">
<div class="mb-3">
<label for="json-file" class="form-label">Fichier JSON des favoris</label>
<input type="file" class="form-control" id="json-file" name="json_file" accept=".json" required>
</div>
<button type="submit" class="btn btn-primary">Analyser le fichier</button>
</form>
<div id="file-summary" class="alert alert-info mt-3 d-none"></div>
</div>
</div>
<!-- Étape 2: Gestion des tokens -->
<div class="card shadow-sm mb-4" id="tokens">
<div class="card-header text-white">
<h3 class="card-title h5 mb-0">2. Configurer vos jetons d'accès</h3>
</div>
<div class="card-body">
<?php if (!$has_primary_token): ?>
<!-- Formulaire de connexion pour le token principal -->
<div class="card bg-light mb-4">
<div class="card-body">
<h4>Token principal</h4>
<p>Commencez par configurer votre jeton d'accès principal en vous connectant à votre instance Misskey.</p>
<form id="misskey-form" method="post" class="mt-3">
<input type="hidden" name="action" value="connect_token">
<div class="mb-3">
<label for="misskey-instance" class="form-label">Instance Misskey</label>
<input type="text" class="form-control" id="misskey-instance" name="misskey_instance"
placeholder="misskey.io" required>
<div class="form-text">Entrez le nom de domaine de votre instance Misskey (ex: misskey.io)</div>
</div>
<div class="mb-3">
<label for="misskey-token" class="form-label">Jeton d'accès</label>
<input type="password" class="form-control" id="misskey-token" name="misskey_token"
placeholder="Votre jeton d'accès Misskey" required>
<div class="form-text">Collez le jeton d'accès généré dans les paramètres de votre compte Misskey</div>
</div>
<button type="submit" class="btn btn-primary">Se connecter à Misskey</button>
</form>
</div>
</div>
<?php else: ?>
<!-- Informations sur l'instance connectée -->
<div class="alert alert-success mb-4">
<strong>Connecté à <?php echo htmlspecialchars($instance); ?></strong>
<p class="mb-0">Vous êtes authentifié avec votre token principal.</p>
<div class="mt-2">
<a href="multitokens.php?action=logout" class="btn btn-sm btn-outline-dark">Déconnexion complète</a>
</div>
</div>
<!-- Gestion des tokens supplémentaires -->
<div class="card mb-4">
<div class="card-header">
<h4 class="h5 mb-0">Tokens supplémentaires</h4>
</div>
<div class="card-body">
<p>Pour accélérer le transfert, ajoutez des jetons d'accès supplémentaires de votre compte Misskey.</p>
<div class="alert alert-info">
<strong>Conseil :</strong> Pour créer plusieurs jetons, connectez-vous à votre instance Misskey, allez dans <strong>Paramètres > API</strong> et générez plusieurs jetons d'accès avec les mêmes permissions que votre token principal.
</div>
<form method="post" class="mb-4">
<input type="hidden" name="action" value="add_token">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label for="token-name" class="form-label">Nom du jeton</label>
<input type="text" class="form-control" id="token-name" name="token_name" placeholder="Token 1" required>
</div>
<div class="col-md-6">
<label for="token-value" class="form-label">Valeur du jeton</label>
<input type="password" class="form-control" id="token-value" name="token_value" placeholder="Jeton d'accès supplémentaire" required>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Ajouter</button>
</div>
</div>
</form>
<h5>Tokens configurés (<?php echo $total_tokens_count; ?>)</h5>
<div id="tokens-container">
<!-- Token principal -->
<div class="card mb-2 token-card">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="token-status ready"></span>
<strong>Token principal</strong>
<span class="badge bg-primary ms-2">Principal</span>
</div>
<div>
<span class="text-muted small">Prêt</span>
</div>
</div>
</div>
</div>
<!-- Tokens supplémentaires -->
<?php if (isset($_SESSION['additional_tokens']) && !empty($_SESSION['additional_tokens'])): ?>
<?php foreach ($_SESSION['additional_tokens'] as $token_id => $token_data): ?>
<div class="card mb-2 token-card" data-token-id="<?php echo $token_id; ?>">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="token-status ready"></span>
<strong><?php echo htmlspecialchars($token_data['name']); ?></strong>
<?php if (isset($token_data['usage_count']) && $token_data['usage_count'] > 0): ?>
<span class="badge bg-secondary ms-2"><?php echo $token_data['usage_count']; ?> utilisations</span>
<?php endif; ?>
</div>
<div class="d-flex align-items-center">
<span class="text-muted small me-3">Prêt</span>
<form method="post" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce jeton ?');">
<input type="hidden" name="action" value="remove_token">
<input type="hidden" name="token_id" value="<?php echo $token_id; ?>">
<button type="submit" class="btn btn-sm btn-outline-danger">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="alert alert-warning">
<p class="mb-0">Aucun token supplémentaire configuré. Ajoutez-en pour accélérer le processus.</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Étape 3: Migration avec tokens multiples -->
<div class="card shadow-sm mb-4" id="step3">
<div class="card-header text-white">
<h3 class="card-title h5 mb-0">3. Migration avec rotation des jetons</h3>
</div>
<div class="card-body">
<?php if ($has_primary_token && $total_tokens_count > 0): ?>
<!-- Configuration de la migration -->
<div class="mb-4">
<h4>Configuration</h4>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="delay-between-tokens" class="form-label">Délai entre deux jetons (secondes)</label>
<input type="range" class="form-range" id="delay-between-tokens" min="5" max="60" value="15" step="5">
<div class="d-flex justify-content-between">
<span class="small">5s</span>
<span id="delay-value">15s</span>
<span class="small">60s</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="token-cooldown" class="form-label">Délai avant réutilisation d'un jeton (secondes)</label>
<input type="range" class="form-range" id="token-cooldown" min="60" max="300" value="150" step="30">
<div class="d-flex justify-content-between">
<span class="small">60s</span>
<span id="cooldown-value">150s</span>
<span class="small">5min</span>
</div>
</div>
</div>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="auto-adjust" checked>
<label class="form-check-label" for="auto-adjust">Ajustement automatique des délais en cas d'erreur</label>
</div>
</div>
<!-- Barre de progression -->
<div class="mb-4">
<h4>Progression</h4>
<div class="mb-3">
<label class="form-label">Progression globale</label>
<div class="progress" style="height: 20px;">
<div id="global-progress" class="progress-bar" role="progressbar"
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
</div>
<div class="mb-3">
<label class="form-label d-flex justify-content-between">
<span>Statistiques</span>
<span id="stats-display">-</span>
</label>
<div class="progress" style="height: 10px;">
<div id="success-progress" class="progress-bar bg-success" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<div id="skip-progress" class="progress-bar bg-info" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<div id="warn-progress" class="progress-bar bg-warning" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<div id="error-progress" class="progress-bar bg-danger" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Journal des opérations</label>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-log">Effacer</button>
</div>
<div id="log-container" class="border p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
<div id="operation-log"></div>
</div>
</div>
</div>
<!-- Contrôles de la migration -->
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-primary" id="start-migration">Démarrer la migration</button>
<div>
<button type="button" class="btn btn-warning d-none" id="pause-migration">Pause</button>
<button type="button" class="btn btn-danger" id="cancel-migration">Annuler</button>
</div>
</div>
<?php else: ?>
<div class="alert alert-warning">
<h5>Configuration incomplète</h5>
<p>Pour démarrer la migration, vous devez :</p>
<ol>
<li>Télécharger votre fichier d'export de favoris</li>
<li>Configurer au moins un jeton d'accès</li>
</ol>
<p class="mb-0">Complétez les étapes précédentes avant de continuer.</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/multitokens.js"></script>
</body>
</html>

@ -1,6 +1,6 @@
<?php
/**
* FavMasToKey - Traitement des favoris
* FavMasToKey - Traitement des favoris (version améliorée)
*/
// Définir la constante pour inclure les fichiers
@ -64,18 +64,43 @@ try {
exit;
}
// Journaliser les paramètres reçus
$receivedParams = [
'tortoiseMode' => isset($input['tortoiseMode']) ? $input['tortoiseMode'] : 'non défini',
'slowMode' => isset($input['slowMode']) ? $input['slowMode'] : 'non défini',
'delaySeconds' => isset($input['delaySeconds']) ? $input['delaySeconds'] : 'non défini',
'useCachedIds' => isset($input['useCachedIds']) ? $input['useCachedIds'] : false,
'hasBatchData' => isset($input['batchData']) ? true : false
];
error_log("Paramètres reçus dans process.php: " . json_encode($receivedParams));
// Récupérer le lot à traiter
$batch = $input['batch'];
$currentIndex = isset($input['currentIndex']) ? (int)$input['currentIndex'] : 0;
$totalItems = isset($input['totalItems']) ? (int)$input['totalItems'] : count($batch);
$slowMode = isset($input['slowMode']) ? (bool)$input['slowMode'] : false;
$tortoiseMode = isset($input['tortoiseMode']) ? (bool)$input['tortoiseMode'] : false;
$delaySeconds = isset($input['delaySeconds']) ? (int)$input['delaySeconds'] : 30;
$useCachedIds = isset($input['useCachedIds']) ? (bool)$input['useCachedIds'] : false;
// AMÉLIORATION 3: Récupérer les données du cache si fournies
$batchData = isset($input['batchData']) ? $input['batchData'] : null;
error_log("Paramètres après conversion: slowMode=" . ($slowMode ? 'true' : 'false') .
", tortoiseMode=" . ($tortoiseMode ? 'true' : 'false') .
", delaySeconds=" . $delaySeconds .
", useCachedIds=" . ($useCachedIds ? 'true' : 'false'));
// Ajuster le délai entre les requêtes selon le mode et la valeur personnalisée
if ($slowMode) {
$delayMs = $delaySeconds * 1000; // Convertir en millisecondes
if ($tortoiseMode) {
$delayMs = $delaySeconds * 1000; // Convertir en millisecondes (délai tortue - 60-300 secondes)
error_log("Utilisation du mode tortue avec délai: " . $delayMs . "ms");
} else if ($slowMode) {
$delayMs = $delaySeconds * 1000; // Convertir en millisecondes (délai lent - 10-60 secondes)
error_log("Utilisation du mode lent avec délai: " . $delayMs . "ms");
} else {
$delayMs = $config['delay_between_requests'];
$delayMs = $config['delay_between_requests']; // Délai normal (3000ms par défaut)
error_log("Utilisation du mode normal avec délai: " . $delayMs . "ms");
}
// Résultats du traitement
@ -88,7 +113,7 @@ try {
// Vérifier que l'URL est valide
if (!$urlParts || !isset($urlParts['host']) || !isset($urlParts['path'])) {
$results[] = [
$results[$index] = [
'status' => 'error',
'message' => "URL invalide: $url",
'error_type' => 'invalid_url'
@ -97,125 +122,242 @@ try {
}
// Ajouter un log pour déboguer
error_log("Recherche de l'URL: " . $url . " sur l'instance: " . $misskey_instance);
error_log("Traitement de l'URL: " . $url . " sur l'instance: " . $misskey_instance);
// Rechercher la note sur Misskey à partir de l'URL
$searchResult = search_federated_note($misskey_instance, $url, $token);
// Vérifier si la recherche a réussi ET si les données contiennent un ID valide
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
// Vérifier et extraire l'ID de manière sécurisée
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
$noteId = $searchResult['data']['id'];
// Tenter d'ajouter la note aux favoris
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
if ($favoriteResult['success']) {
$results[] = [
'status' => 'success',
'message' => "Ajouté aux favoris: $url"
];
} else {
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
$errorMessage = isset($favoriteResult['data']['error']['message'])
? $favoriteResult['data']['error']['message']
: (isset($favoriteResult['message']) ? $favoriteResult['message'] : 'Erreur inconnue');
// AMÉLIORATION 3: Vérifier si nous avons un ID en cache pour cette URL
$cachedMisskeyId = null;
if ($useCachedIds && $batchData) {
// Vérifier si batchData est un tableau indexé ou associatif
if (isset($batchData[$index])) {
// Format indexé
if ($batchData[$index]['url'] === $url &&
$batchData[$index]['cached'] === true) {
// Extraire le code d'erreur si disponible
$errorCode = isset($favoriteResult['data']['error']['code'])
? $favoriteResult['data']['error']['code']
: '';
// Déterminer le type d'erreur
$errorType = 'api_error';
if (strpos($errorMessage, 'already') !== false) {
$errorType = 'already_favorited';
} elseif (strpos($errorMessage, 'rate limit') !== false || strpos($errorMessage, 'limit exceeded') !== false || $errorCode == 'RATE_LIMIT_EXCEEDED') {
$errorType = 'rate_limit';
} elseif (strpos($errorMessage, 'permission') !== false || $errorCode == 'NO_PERMISSION') {
$errorType = 'permission_denied';
}
if ($errorType === 'already_favorited') {
$results[] = [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url",
'error_type' => $errorType
];
} else if ($errorType === 'rate_limit') {
$results[] = [
'status' => 'warning',
'message' => "Limite d'API atteinte pour: $url. " . ($slowMode ? "Essayez d'augmenter le délai." : "Activez le mode ultra-lent."),
'error_type' => $errorType,
'details' => $errorMessage
];
} else {
$results[] = [
'status' => 'error',
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage",
'error_type' => $errorType,
'details' => $errorCode ? "Code: $errorCode" : ""
];
}
$cachedMisskeyId = $batchData[$index]['cachedId'];
error_log("ID trouvé en cache pour $url: $cachedMisskeyId");
}
} else {
// L'ID est manquant ou vide dans la réponse
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
$results[] = [
'status' => 'error',
'message' => "Note trouvée mais sans ID valide: $url",
'error_type' => 'invalid_response',
'details' => "Clés disponibles: " . implode(', ', $dataKeys)
];
// Log pour déboguer
error_log("Structure de données reçue: " . json_encode($searchResult['data']));
}
} else {
// Note non trouvée ou erreur de recherche
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
$errorCode = isset($searchResult['error_code']) ? $searchResult['error_code'] : '';
$httpCode = isset($searchResult['http_code']) ? $searchResult['http_code'] : '';
// Déterminer le type d'erreur
$errorType = 'not_found';
if (strpos($errorMessage, 'rate limit') !== false || $httpCode == 429 ||
(isset($searchResult['data']['error']) &&
(isset($searchResult['data']['error']['code']) && $searchResult['data']['error']['code'] == 'RATE_LIMIT_EXCEEDED'))) {
$errorType = 'rate_limit';
$errorMessage = $slowMode
? "Limite d'API atteinte malgré le mode ultra-lent. Essayez d'augmenter le délai ou attendez quelques minutes."
: "Limite d'API atteinte. Activez le mode ultra-lent ou attendez quelques minutes.";
} elseif ($httpCode >= 500) {
$errorType = 'server_error';
$errorMessage = "L'instance Misskey rencontre des problèmes. Statut HTTP: $httpCode";
} elseif ($httpCode == 401 || $httpCode == 403) {
$errorType = 'auth_error';
$errorMessage = "Problème d'authentification ou de permission. Vérifiez votre token.";
} elseif ($httpCode == 0) {
$errorType = 'network_error';
$errorMessage = "Problème de connexion réseau. Vérifiez votre connectivité.";
}
$results[] = [
'status' => $errorType == 'rate_limit' ? 'warning' : 'error',
'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)",
'error_type' => $errorType,
'details' => $httpCode ? "HTTP: $httpCode" : ""
];
// Ajouter des infos de debug
if (isset($searchResult['data']) && is_array($searchResult['data'])) {
error_log("Données reçues pour URL $url: " . json_encode($searchResult['data']));
// Format non-indexé, parcourir et chercher l'URL
foreach ($batchData as $item) {
if (isset($item['url']) && $item['url'] === $url &&
isset($item['cached']) && $item['cached'] === true &&
isset($item['cachedId'])) {
$cachedMisskeyId = $item['cachedId'];
error_log("ID trouvé en cache pour $url: $cachedMisskeyId (recherche)");
break;
}
}
}
}
// Pause pour éviter le rate limiting - utilise le délai approprié selon le mode
usleep($delayMs * 1000);
// AMÉLIORATION 2: Si nous avons un ID en cache, nous pouvons sauter directement à la vérification du statut de favori
$noteId = null;
$searchNeeded = true;
if ($cachedMisskeyId) {
$noteId = $cachedMisskeyId;
$searchNeeded = false;
error_log("Utilisation de l'ID en cache, recherche fédérée évitée pour: $url");
}
// Rechercher la note sur Misskey à partir de l'URL si nécessaire
if ($searchNeeded) {
$searchResult = search_federated_note($misskey_instance, $url, $token);
// Vérifier si la recherche a réussi ET si les données contiennent un ID valide
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
// Vérifier et extraire l'ID de manière sécurisée
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
$noteId = $searchResult['data']['id'];
// AMÉLIORATION 3: Sauvegarder le résultat dans la réponse pour mise à jour du cache côté client
$results[$index]['misskey_id'] = $noteId;
} else {
// L'ID est manquant ou vide dans la réponse
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
$results[$index] = [
'status' => 'error',
'message' => "Note trouvée mais sans ID valide: $url",
'error_type' => 'invalid_response',
'details' => "Clés disponibles: " . implode(', ', $dataKeys)
];
// Log pour déboguer
error_log("Structure de données reçue: " . json_encode($searchResult['data']));
continue;
}
} else {
// Note non trouvée ou erreur de recherche
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
$errorCode = isset($searchResult['error_code']) ? $searchResult['error_code'] : '';
$httpCode = isset($searchResult['http_code']) ? $searchResult['http_code'] : '';
// Déterminer le type d'erreur
$errorType = 'not_found';
if (strpos($errorMessage, 'rate limit') !== false || $httpCode == 429 ||
(isset($searchResult['data']['error']) &&
(isset($searchResult['data']['error']['code']) && $searchResult['data']['error']['code'] == 'RATE_LIMIT_EXCEEDED'))) {
$errorType = 'rate_limit';
if ($tortoiseMode) {
$errorMessage = "Limite d'API atteinte malgré le mode tortue. Essayez d'augmenter le délai ou attendez quelques minutes.";
} else if ($slowMode) {
$errorMessage = "Limite d'API atteinte. Activez le mode tortue ou attendez quelques minutes.";
} else {
$errorMessage = "Limite d'API atteinte. Activez le mode ultra-lent ou le mode tortue.";
}
} elseif ($httpCode >= 500) {
$errorType = 'server_error';
$errorMessage = "L'instance Misskey rencontre des problèmes. Statut HTTP: $httpCode";
} elseif ($httpCode == 401 || $httpCode == 403) {
$errorType = 'auth_error';
$errorMessage = "Problème d'authentification ou de permission. Vérifiez votre token.";
} elseif ($httpCode == 0) {
$errorType = 'network_error';
$errorMessage = "Problème de connexion réseau. Vérifiez votre connectivité.";
}
$results[$index] = [
'status' => $errorType == 'rate_limit' ? 'warning' : 'error',
'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)",
'error_type' => $errorType,
'details' => $httpCode ? "HTTP: $httpCode" : ""
];
// Ajouter des infos de debug
if (isset($searchResult['data']) && is_array($searchResult['data'])) {
error_log("Données reçues pour URL $url: " . json_encode($searchResult['data']));
}
continue;
}
}
// AMÉLIORATION 2: Vérifier d'abord si la note est déjà favorite pour économiser une requête API
$favoriteCheckResult = check_if_favorited($misskey_instance, $noteId, $token);
if ($favoriteCheckResult['success'] && $favoriteCheckResult['is_favorited']) {
// La note est déjà dans les favoris
$results[$index] = [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url",
'error_type' => 'already_favorited',
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID même si déjà favori
];
// AMÉLIORATION 2: Ne pas attendre le délai complet pour les notes déjà favorites
error_log("Note déjà favorite, aucun délai nécessaire: $url");
continue;
} else {
// La note n'est pas encore dans les favoris ou la vérification a échoué
// Dans le cas d'échec, nous essayons quand même d'ajouter la note
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
if ($favoriteResult['success']) {
$results[$index] = [
'status' => 'success',
'message' => "Ajouté aux favoris: $url",
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID pour mise à jour du cache
];
} else {
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
$errorMessage = isset($favoriteResult['data']['error']['message'])
? $favoriteResult['data']['error']['message']
: (isset($favoriteResult['message']) ? $favoriteResult['message'] : 'Erreur inconnue');
// Extraire le code d'erreur si disponible
$errorCode = isset($favoriteResult['data']['error']['code'])
? $favoriteResult['data']['error']['code']
: '';
// Déterminer le type d'erreur
$errorType = 'api_error';
if (strpos($errorMessage, 'already') !== false) {
$errorType = 'already_favorited';
} elseif (strpos($errorMessage, 'rate limit') !== false || strpos($errorMessage, 'limit exceeded') !== false || $errorCode == 'RATE_LIMIT_EXCEEDED') {
$errorType = 'rate_limit';
} elseif (strpos($errorMessage, 'permission') !== false || $errorCode == 'NO_PERMISSION') {
$errorType = 'permission_denied';
}
if ($errorType === 'already_favorited') {
$results[$index] = [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url",
'error_type' => $errorType,
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID même si déjà favori
];
} else if ($errorType === 'rate_limit') {
$message = "Limite d'API atteinte pour: $url. ";
if ($tortoiseMode) {
$message .= "Essayez d'augmenter le délai ou attendez quelques minutes.";
} else if ($slowMode) {
$message .= "Activez le mode tortue.";
} else {
$message .= "Activez le mode ultra-lent ou le mode tortue.";
}
$results[$index] = [
'status' => 'warning',
'message' => $message,
'error_type' => $errorType,
'details' => $errorMessage,
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID pour mise à jour du cache
];
} else {
$results[$index] = [
'status' => 'error',
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage",
'error_type' => $errorType,
'details' => $errorCode ? "Code: $errorCode" : "",
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID pour mise à jour du cache
];
}
}
}
// AMÉLIORATION 2: Pas besoin de pause pour les notes déjà dans les favoris ou si nous avons utilisé l'ID en cache
// Nous vérifions si l'opération actuelle nécessite un délai
$requiresDelay = true;
// Pas de délai nécessaire si c'est déjà en favoris
if ($results[$index]['status'] === 'info' && $results[$index]['error_type'] === 'already_favorited') {
$requiresDelay = false;
}
// Pas de délai nécessaire si nous avons utilisé un ID en cache et l'opération a réussi
if ($cachedMisskeyId && ($results[$index]['status'] === 'success' ||
($results[$index]['status'] === 'info' && $results[$index]['error_type'] === 'already_favorited'))) {
$requiresDelay = false;
}
// Pour les notes qui requièrent vraiment un délai (opérations lourdes d'API)
if ($requiresDelay) {
// En mode tortue ou lent, appliquer un délai réduit pour les notes où nous avons fait moins de requêtes API
if (($tortoiseMode || $slowMode) && !$searchNeeded) {
// Si nous avons sauté la recherche fédérée, utiliser seulement 10% du délai complet
$reducedDelay = floor($delayMs * 0.1);
error_log("Pause réduite de {$reducedDelay}ms pour note avec ID connu: $url");
usleep($reducedDelay * 1000);
} else {
// Délai complet pour les requêtes complètes qui nécessitent une recherche fédérée
error_log("Pause complète de {$delayMs}ms après traitement complet de: $url");
usleep($delayMs * 1000);
}
} else {
error_log("Aucune pause nécessaire pour la note: $url");
}
}
// Si au moins une erreur de rate limit a été détectée, suggérer le mode lent
// AMÉLIORATION 2: Vérifier si toutes les notes sont déjà favorites
$allAlreadyFavorited = true;
foreach ($results as $result) {
if (!($result['status'] === 'info' && $result['error_type'] === 'already_favorited')) {
$allAlreadyFavorited = false;
break;
}
}
// Si au moins une erreur de rate limit a été détectée, suggérer le mode approprié
$hasRateLimitErrors = false;
foreach ($results as $result) {
if (isset($result['error_type']) && $result['error_type'] === 'rate_limit') {
@ -224,6 +366,14 @@ try {
}
}
// Déterminer les suggestions à faire à l'utilisateur
$suggestions = [
'use_slow_mode' => $hasRateLimitErrors && !$slowMode && !$tortoiseMode,
'use_tortoise_mode' => $hasRateLimitErrors && $slowMode && !$tortoiseMode,
'increase_delay' => $hasRateLimitErrors && ($slowMode || $tortoiseMode) && $delaySeconds < ($tortoiseMode ? 300 : 60),
'all_already_favorited' => $allAlreadyFavorited // AMÉLIORATION 2: Indiquer si toutes les notes sont déjà favorites
];
// Renvoyer les résultats
echo json_encode([
'success' => true,
@ -233,13 +383,12 @@ try {
'total' => $totalItems,
'percentage' => round((($currentIndex + count($batch)) / $totalItems) * 100, 2)
],
'suggestions' => [
'use_slow_mode' => $hasRateLimitErrors && !$slowMode,
'increase_delay' => $hasRateLimitErrors && $slowMode && $delaySeconds < 60
],
'suggestions' => $suggestions,
'config' => [
'slow_mode' => $slowMode,
'delay_seconds' => $delaySeconds
'tortoise_mode' => $tortoiseMode,
'delay_seconds' => $delaySeconds,
'all_already_favorited' => $allAlreadyFavorited // AMÉLIORATION 2: Indiquer dans la config si tout est déjà traité
]
]);

308
process_multitoken.php Normal file

@ -0,0 +1,308 @@
<?php
/**
* FavMasToKey - Traitement des favoris (version multi-tokens)
*/
// Définir la constante pour inclure les fichiers
define('FAVMASTOKEY', true);
// Forcer les en-têtes JSON dès le début pour éviter tout conflit
header('Content-Type: application/json');
// Activer la capture d'erreurs
set_error_handler(function($errno, $errstr, $errfile, $errline) {
echo json_encode([
'success' => false,
'message' => "Erreur PHP: $errstr (ligne $errline dans $errfile)",
]);
exit;
});
try {
// Inclure les fichiers requis
require_once 'includes/config.php';
require_once 'includes/functions.php';
// Vérifier que la requête est en POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']);
exit;
}
// Récupérer les données envoyées
$input_data = file_get_contents('php://input');
$input = json_decode($input_data, true);
if (!$input || !isset($input['url'])) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Données invalides',
'debug' => [
'received' => $input_data ? substr($input_data, 0, 200) . '...' : 'Aucune donnée reçue'
]
]);
exit;
}
// Récupérer l'instance Misskey (devrait être la même pour tous les tokens)
$misskey_instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
if (empty($misskey_instance)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Instance Misskey non définie']);
exit;
}
// Récupérer l'URL à traiter
$url = $input['url'];
// Récupérer l'ID du token à utiliser
$token_id = isset($input['token_id']) ? $input['token_id'] : 'primary';
// Récupérer le token d'accès selon l'ID fourni
if ($token_id === 'primary') {
// Utiliser le token principal
if (!isset($_SESSION['misskey_token']) || empty($_SESSION['misskey_token'])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Token principal non disponible']);
exit;
}
$token = $_SESSION['misskey_token'];
} else {
// Utiliser un token supplémentaire
if (!isset($_SESSION['additional_tokens']) || !isset($_SESSION['additional_tokens'][$token_id])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Token supplémentaire non trouvé: ' . $token_id]);
exit;
}
$token = $_SESSION['additional_tokens'][$token_id]['token'];
// Mettre à jour les statistiques d'utilisation du token
$_SESSION['additional_tokens'][$token_id]['last_used'] = time();
$_SESSION['additional_tokens'][$token_id]['usage_count'] =
(isset($_SESSION['additional_tokens'][$token_id]['usage_count']) ?
$_SESSION['additional_tokens'][$token_id]['usage_count'] : 0) + 1;
}
// Journaliser le début du traitement
error_log("Traitement de l'URL: " . $url . " avec token ID: " . $token_id);
// Traitement de l'URL
// 1. Vérifier si nous avons un ID en cache pour cette URL
$cachedMisskeyId = null;
$federatedCache = get_federated_cache();
if (isset($federatedCache[$url])) {
$cachedMisskeyId = $federatedCache[$url]['id'];
error_log("ID trouvé en cache pour $url: $cachedMisskeyId");
}
// 2. Recherche de la note sur le réseau fédéré
$noteId = null;
$searchNeeded = true;
if ($cachedMisskeyId) {
$noteId = $cachedMisskeyId;
$searchNeeded = false;
error_log("Utilisation de l'ID en cache, recherche fédérée évitée pour: $url");
}
if ($searchNeeded) {
$searchResult = search_federated_note($misskey_instance, $url, $token);
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
$noteId = $searchResult['data']['id'];
// Sauvegarder dans le cache
update_federated_cache($url, $noteId);
error_log("Note trouvée et mise en cache: $url -> $noteId");
} else {
// L'ID est manquant dans la réponse
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
echo json_encode([
'success' => true,
'result' => [
'status' => 'error',
'message' => "Note trouvée mais sans ID valide: $url",
'error_type' => 'invalid_response',
'details' => "Clés disponibles: " . implode(', ', $dataKeys)
]
]);
exit;
}
} else {
// Note non trouvée ou erreur de recherche
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
$errorCode = isset($searchResult['error_code']) ? $searchResult['error_code'] : '';
$httpCode = isset($searchResult['http_code']) ? $searchResult['http_code'] : '';
// Déterminer le type d'erreur
$errorType = 'not_found';
if (strpos($errorMessage, 'rate limit') !== false || $httpCode == 429 ||
(isset($searchResult['data']['error']) &&
isset($searchResult['data']['error']['code']) && $searchResult['data']['error']['code'] == 'RATE_LIMIT_EXCEEDED')) {
$errorType = 'rate_limit';
$errorMessage = "Limite d'API atteinte. Ce jeton doit se reposer.";
} elseif ($httpCode >= 500) {
$errorType = 'server_error';
$errorMessage = "L'instance Misskey rencontre des problèmes. Statut HTTP: $httpCode";
} elseif ($httpCode == 401 || $httpCode == 403) {
$errorType = 'auth_error';
$errorMessage = "Problème d'authentification ou de permission avec ce jeton.";
} elseif ($httpCode == 0) {
$errorType = 'network_error';
$errorMessage = "Problème de connexion réseau. Vérifiez votre connectivité.";
}
echo json_encode([
'success' => true,
'result' => [
'status' => $errorType == 'rate_limit' ? 'warning' : 'error',
'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)",
'error_type' => $errorType,
'details' => $httpCode ? "HTTP: $httpCode" : ""
]
]);
exit;
}
}
// 3. Vérifier si la note est déjà favorite
$favoriteCheckResult = check_if_favorited($misskey_instance, $noteId, $token);
if ($favoriteCheckResult['success'] && $favoriteCheckResult['is_favorited']) {
// La note est déjà dans les favoris
echo json_encode([
'success' => true,
'result' => [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url",
'error_type' => 'already_favorited',
'misskey_id' => $noteId
]
]);
exit;
}
// 4. Ajouter la note aux favoris
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
if ($favoriteResult['success']) {
echo json_encode([
'success' => true,
'result' => [
'status' => 'success',
'message' => "Ajouté aux favoris: $url",
'misskey_id' => $noteId
]
]);
} else {
// Déterminer le type d'erreur
$errorMessage = isset($favoriteResult['data']['error']['message'])
? $favoriteResult['data']['error']['message']
: (isset($favoriteResult['message']) ? $favoriteResult['message'] : 'Erreur inconnue');
// Extraire le code d'erreur si disponible
$errorCode = isset($favoriteResult['data']['error']['code'])
? $favoriteResult['data']['error']['code']
: '';
// Déterminer le type d'erreur
$errorType = 'api_error';
if (strpos($errorMessage, 'already') !== false) {
$errorType = 'already_favorited';
echo json_encode([
'success' => true,
'result' => [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url",
'error_type' => $errorType,
'misskey_id' => $noteId
]
]);
exit;
} elseif (strpos($errorMessage, 'rate limit') !== false || strpos($errorMessage, 'limit exceeded') !== false || $errorCode == 'RATE_LIMIT_EXCEEDED') {
$errorType = 'rate_limit';
$message = "Limite d'API atteinte pour: $url. Ce jeton doit se reposer.";
echo json_encode([
'success' => true,
'result' => [
'status' => 'warning',
'message' => $message,
'error_type' => $errorType,
'details' => $errorMessage,
'misskey_id' => $noteId
]
]);
exit;
} elseif (strpos($errorMessage, 'permission') !== false || $errorCode == 'NO_PERMISSION') {
$errorType = 'permission_denied';
}
echo json_encode([
'success' => true,
'result' => [
'status' => 'error',
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage",
'error_type' => $errorType,
'details' => $errorCode ? "Code: $errorCode" : "",
'misskey_id' => $noteId
]
]);
}
} catch (Exception $e) {
// Capturer toutes les exceptions et renvoyer un message d'erreur formaté en JSON
echo json_encode([
'success' => false,
'message' => 'Exception: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}
/**
* Récupère le cache de fédération depuis la session
*
* @return array Cache de fédération
*/
function get_federated_cache() {
if (!isset($_SESSION['federated_cache'])) {
$_SESSION['federated_cache'] = [];
}
return $_SESSION['federated_cache'];
}
/**
* Met à jour le cache de fédération dans la session
*
* @param string $url URL de la publication Mastodon
* @param string $misskey_id ID de la publication sur Misskey
*/
function update_federated_cache($url, $misskey_id) {
if (!isset($_SESSION['federated_cache'])) {
$_SESSION['federated_cache'] = [];
}
$_SESSION['federated_cache'][$url] = [
'id' => $misskey_id,
'timestamp' => time()
];
// Nettoyer le cache si nécessaire (plus de 1000 entrées)
if (count($_SESSION['federated_cache']) > 1000) {
// Trier par timestamp et ne garder que les 500 plus récentes
uasort($_SESSION['federated_cache'], function($a, $b) {
return $b['timestamp'] - $a['timestamp'];
});
$_SESSION['federated_cache'] = array_slice($_SESSION['federated_cache'], 0, 500, true);
}
}