Compare commits

...

3 Commits
0.5.0 ... 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
10 changed files with 2977 additions and 324 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.5.0', // Mise à jour de la version pour le mode tortue
'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
@ -42,17 +42,43 @@ $config = [
'timeout' => 90,
'max_retries' => 3,
'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)
'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
// 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
@ -87,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;
}
}
@ -260,8 +314,8 @@ function check_if_favorited($instance, $note_id, $token) {
'noteId' => $note_id
];
// 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);
if ($result['success'] && isset($result['data'])) {
// La note existe et nous avons une réponse
@ -270,13 +324,15 @@ function check_if_favorited($instance, $note_id, $token) {
return [
'success' => true,
'is_favorited' => true,
'message' => 'Cette note est déjà dans vos favoris'
'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'
'message' => 'Cette note n\'est pas encore dans vos favoris',
'timing' => isset($result['timing']) ? $result['timing'] : null
];
}
}
@ -286,7 +342,8 @@ function check_if_favorited($instance, $note_id, $token) {
'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'
'error_code' => isset($result['error_code']) ? $result['error_code'] : 'UNKNOWN_ERROR',
'timing' => isset($result['timing']) ? $result['timing'] : null
];
}
@ -312,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'])) {
@ -360,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'])));
@ -401,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;
@ -416,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
@ -441,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
];
}
@ -464,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 -->

635
js/app.js

@ -1,5 +1,5 @@
/**
* FavMasToKey - Script JavaScript principal
* FavMasToKey - Script JavaScript principal (version améliorée)
*/
// Attendre que le DOM soit chargé
@ -70,6 +70,235 @@ document.addEventListener('DOMContentLoaded', function() {
}
};
// AMÉLIORATION 1: File d'attente pour les favoris en échec de rate-limit
let rateLimitQueue = [];
// AMÉLIORATION 3: Cache pour les résultats de recherche fédérée
const federatedCache = {
// Structure: { "mastodon_url": { id: "misskey_id", timestamp: Date.now() } }
cache: {},
// Ajouter une entrée au cache
add: function(mastodonUrl, misskeyId) {
this.cache[mastodonUrl] = {
id: misskeyId,
timestamp: Date.now()
};
this.saveToStorage();
},
// Récupérer une entrée du cache
get: function(mastodonUrl) {
if (this.cache[mastodonUrl]) {
return this.cache[mastodonUrl].id;
}
return null;
},
// Vérifier si une URL est dans le cache
has: function(mastodonUrl) {
return this.cache.hasOwnProperty(mastodonUrl);
},
// Sauvegarder le cache dans localStorage
saveToStorage: function() {
try {
localStorage.setItem('favmastokey_federated_cache', JSON.stringify(this.cache));
} catch (e) {
// En cas d'erreur (ex: quota dépassé), nettoyer le cache ancien
this.cleanup(180); // Nettoyer les entrées plus anciennes que 3 heures
try {
localStorage.setItem('favmastokey_federated_cache', JSON.stringify(this.cache));
} catch (e) {
console.error("Impossible de sauvegarder le cache même après nettoyage", e);
}
}
},
// Charger le cache depuis localStorage
loadFromStorage: function() {
const savedCache = localStorage.getItem('favmastokey_federated_cache');
if (savedCache) {
try {
this.cache = JSON.parse(savedCache);
// Nettoyer les entrées trop anciennes au chargement
this.cleanup(1440); // 24 heures
} catch (e) {
console.error("Erreur lors du chargement du cache", e);
this.cache = {};
}
}
},
// Nettoyer les entrées trop anciennes (en minutes)
cleanup: function(maxAgeMinutes) {
const now = Date.now();
const maxAge = maxAgeMinutes * 60 * 1000;
Object.keys(this.cache).forEach(url => {
if (now - this.cache[url].timestamp > maxAge) {
delete this.cache[url];
}
});
}
};
// AMÉLIORATION 4: Suivi des performances API
const apiPerformance = {
// Statistiques par domaine
domainStats: {},
// Ajouter une mesure de temps de réponse
addResponseTime: function(domain, responseTimeMs, success) {
if (!this.domainStats[domain]) {
this.domainStats[domain] = {
requestCount: 0,
successCount: 0,
failureCount: 0,
rateLimitCount: 0,
totalResponseTime: 0,
avgResponseTime: 0,
lastRateLimitTime: null,
recommendedDelay: null
};
}
const stats = this.domainStats[domain];
stats.requestCount++;
stats.totalResponseTime += responseTimeMs;
stats.avgResponseTime = stats.totalResponseTime / stats.requestCount;
if (success === true) {
stats.successCount++;
} else if (success === false) {
stats.failureCount++;
} else if (success === 'rate_limit') {
stats.rateLimitCount++;
stats.lastRateLimitTime = Date.now();
// Calculer un délai recommandé basé sur les échecs de rate-limit
// Plus de rate-limits = délai plus long
const baseDelay = 30; // Délai de base en secondes
stats.recommendedDelay = baseDelay + (Math.min(5, stats.rateLimitCount) * 30);
}
this.saveToStorage();
return stats;
},
// Obtenir les statistiques pour un domaine
getDomainStats: function(domain) {
return this.domainStats[domain] || null;
},
// Obtenir le délai recommandé pour un domaine
getRecommendedDelay: function(domain) {
const stats = this.getDomainStats(domain);
if (stats && stats.recommendedDelay) {
return stats.recommendedDelay;
}
return null;
},
// Sauvegarder dans localStorage
saveToStorage: function() {
try {
localStorage.setItem('favmastokey_api_performance', JSON.stringify(this.domainStats));
} catch (e) {
console.error("Erreur lors de la sauvegarde des performances API", e);
}
},
// Charger depuis localStorage
loadFromStorage: function() {
const saved = localStorage.getItem('favmastokey_api_performance');
if (saved) {
try {
this.domainStats = JSON.parse(saved);
} catch (e) {
console.error("Erreur lors du chargement des performances API", e);
this.domainStats = {};
}
}
}
};
// AMÉLIORATION 5: Points de sauvegarde intermédiaires
const saveCheckpoints = {
// Intervalle entre les sauvegardes en millisecondes
saveInterval: 5 * 60 * 1000, // 5 minutes par défaut
lastSaveTime: null,
timer: null,
// Démarrer les sauvegardes automatiques
start: function() {
this.lastSaveTime = Date.now();
// Nettoyer l'ancien timer si existant
if (this.timer) {
clearInterval(this.timer);
}
// Configurer un nouveau timer
this.timer = setInterval(() => {
if (isProcessing && !isPaused) {
this.createCheckpoint();
}
}, this.saveInterval);
},
// Arrêter les sauvegardes automatiques
stop: function() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
// Créer un point de sauvegarde
createCheckpoint: function() {
if (!isProcessing) return;
this.lastSaveTime = Date.now();
// Sauvegarder l'état actuel de la migration
updateMigrationData(migration.status);
// Sauvegarder également le cache de fédération et les performances API
federatedCache.saveToStorage();
apiPerformance.saveToStorage();
// Sauvegarder la file d'attente rate-limit si non vide
if (rateLimitQueue.length > 0) {
try {
localStorage.setItem('favmastokey_ratelimit_queue', JSON.stringify(rateLimitQueue));
} catch (e) {
console.error("Erreur lors de la sauvegarde de la file d'attente rate-limit", e);
}
}
addLogEntry("Point de sauvegarde créé automatiquement", "info");
},
// Restaurer depuis un point de sauvegarde
restore: function() {
// La restauration principale est déjà gérée dans le code de démarrage
// Restaurer la file d'attente rate-limit si existante
const savedQueue = localStorage.getItem('favmastokey_ratelimit_queue');
if (savedQueue) {
try {
rateLimitQueue = JSON.parse(savedQueue);
if (rateLimitQueue.length > 0) {
addLogEntry(`Restauration de ${rateLimitQueue.length} favoris en attente après rate-limit`, "info");
}
} catch (e) {
console.error("Erreur lors de la restauration de la file d'attente rate-limit", e);
rateLimitQueue = [];
}
}
}
};
console.log('Éléments DOM initialisés:', {
tortoiseCheckbox: !!tortoiseCheckbox,
tortoiseWarning: !!tortoiseWarning,
@ -77,6 +306,10 @@ document.addEventListener('DOMContentLoaded', function() {
slowModeCheckbox: !!slowModeCheckbox
});
// Initialisation du cache et des statistiques au chargement
federatedCache.loadFromStorage();
apiPerformance.loadFromStorage();
// Gérer l'affichage du message d'avertissement pour le mode lent
if (slowModeCheckbox) {
slowModeCheckbox.addEventListener('change', function() {
@ -401,6 +634,9 @@ document.addEventListener('DOMContentLoaded', function() {
autoPauseEnabled.checked = migration.options.autoPauseEnabled;
}
}
// AMÉLIORATION 5: Restaurer depuis les points de sauvegarde
saveCheckpoints.restore();
}
}
}
@ -453,6 +689,10 @@ document.addEventListener('DOMContentLoaded', function() {
consecutiveRateLimitErrors = 0;
adaptiveDelayIncreases = 0;
// Réinitialiser la file d'attente rate-limit
rateLimitQueue = [];
localStorage.removeItem('favmastokey_ratelimit_queue');
migration = {
status: 'not_started',
startTime: null,
@ -708,6 +948,9 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// AMÉLIORATION 5: Restaurer depuis les points de sauvegarde
saveCheckpoints.restore();
// Afficher un résumé
addLogEntry(`Reprise de la migration: ${successCount} réussis, ${errorCount} échecs, ${skippedCount} ignorés, ${warningCount} avertissements.`, 'info');
} else {
@ -749,6 +992,9 @@ document.addEventListener('DOMContentLoaded', function() {
migration.startTime = Date.now();
}
// Démarrer les sauvegardes automatiques
saveCheckpoints.start();
// Mettre à jour le statut de la migration
updateMigrationData('in_progress');
@ -790,7 +1036,7 @@ document.addEventListener('DOMContentLoaded', function() {
addLogEntry(`Démarrage de la migration... (${modeName})`, 'info');
// Lancer le processus de migration
processBatch();
processNextBatch();
});
}
@ -808,6 +1054,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Mettre à jour le statut de la migration
updateMigrationData('paused');
// Arrêter les sauvegardes automatiques
saveCheckpoints.stop();
} else {
pauseMigration.textContent = 'Pause';
addLogEntry('Reprise de la migration...', 'info');
@ -816,7 +1065,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Mettre à jour le statut de la migration
updateMigrationData('in_progress');
processBatch();
// Redémarrer les sauvegardes automatiques
saveCheckpoints.start();
// Reprendre le traitement
processNextBatch();
}
});
}
@ -837,6 +1090,9 @@ document.addEventListener('DOMContentLoaded', function() {
isPaused = false;
rateLimitPauseActive = false;
// Arrêter les sauvegardes automatiques
saveCheckpoints.stop();
// Annuler le timer de reprise automatique si en cours
if (autoRestartTimer) {
clearTimeout(autoRestartTimer);
@ -859,33 +1115,35 @@ document.addEventListener('DOMContentLoaded', function() {
}
/**
* Traite un lot de favoris
* Point d'entrée principal du processus de traitement des favoris
* Gère à la fois les favoris normaux et ceux en file d'attente après rate-limit
*/
function processBatch() {
if (!isProcessing || isPaused || currentIndex >= totalItems) {
if (currentIndex >= totalItems) {
// Migration terminée
isProcessing = false;
const summary = `Migration terminée ! ${successCount} publications ajoutées aux favoris, ${errorCount} erreurs, ${skippedCount} déjà présentes, ${warningCount} avertissements.`;
addLogEntry(summary, 'success');
startMigration.classList.remove('d-none');
startMigration.textContent = 'Terminer';
startMigration.addEventListener('click', function() {
// Nettoyer localStorage et retourner à l'étape 1
localStorage.removeItem('favmastokey_favorites');
localStorage.removeItem('favmastokey_migration');
step3.classList.add('d-none');
step1.classList.remove('d-none');
});
pauseMigration.classList.add('d-none');
// Mettre à jour la progression à 100%
updateProgress(100);
// Mettre à jour le statut de la migration
updateMigrationData('completed');
function processNextBatch() {
if (!isProcessing || isPaused) {
return;
}
// AMÉLIORATION 1: Vérifier d'abord la file d'attente des favoris en rate-limit
if (rateLimitQueue.length > 0) {
addLogEntry(`Traitement des favoris en attente après rate-limit (${rateLimitQueue.length} restants)`, 'info');
const urlToRetry = rateLimitQueue.shift();
// Enregistrer l'état de la file d'attente dans localStorage
if (rateLimitQueue.length > 0) {
localStorage.setItem('favmastokey_ratelimit_queue', JSON.stringify(rateLimitQueue));
} else {
localStorage.removeItem('favmastokey_ratelimit_queue');
}
// Traiter cette URL spécifique
processBatch([urlToRetry], true);
return;
}
// Vérifier si nous avons terminé
if (currentIndex >= totalItems) {
// Migration terminée
finishMigration();
return;
}
@ -893,36 +1151,49 @@ document.addEventListener('DOMContentLoaded', function() {
let isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked;
let isSlowMode = slowModeCheckbox && slowModeCheckbox.checked;
console.log('État des modes avant traitement du lot:', {
isTortoiseMode,
isSlowMode,
tortoiseCheckboxExists: !!tortoiseCheckbox,
tortoiseCheckboxState: tortoiseCheckbox ? tortoiseCheckbox.checked : 'N/A'
});
// Nombre d'éléments à traiter dans ce lot
const batchSize = (isTortoiseMode || isSlowMode) ? 1 : 2; // Toujours un seul élément en mode lent ou tortue
const batchSize = (isTortoiseMode || isSlowMode) ? 1 : 2;
const endIndex = Math.min(currentIndex + batchSize, totalItems);
// Préparer les éléments du lot
const batch = favoritesList.slice(currentIndex, endIndex);
// Traiter le lot normal
processBatch(batch, false);
}
/**
* Traite un lot de favoris
* @param {Array} batch - URLs à traiter
* @param {boolean} isRetry - Indique si c'est une tentative après rate-limit
*/
function processBatch(batch, isRetry = false) {
if (!isProcessing || isPaused) {
return;
}
// Mettre à jour la progression actuelle
currentProgress.classList.add('active');
updateProgress();
// Ajouter une entrée dans le journal
addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info');
// Ajouter une entrée dans le journal si ce n'est pas une tentative après rate-limit
if (!isRetry) {
const endIndex = Math.min(currentIndex + batch.length, totalItems);
addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info');
}
// Compteur de tentatives pour cette requête
let retryAttempt = 0;
const maxRetries = 3;
// Variables pour mesurer les performances
const requestStartTime = Date.now();
// Fonction pour envoyer la requête avec retry automatique
function sendRequest() {
// Déterminer le délai à utiliser - Assurez-vous que les valeurs sont à jour
isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked;
isSlowMode = slowModeCheckbox && slowModeCheckbox.checked;
let isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked;
let isSlowMode = slowModeCheckbox && slowModeCheckbox.checked;
let delaySeconds = 3;
if (isTortoiseMode && tortoiseDelay) {
@ -931,10 +1202,38 @@ document.addEventListener('DOMContentLoaded', function() {
delaySeconds = parseInt(slowModeDelay.value);
}
// AMÉLIORATION 3: Vérifier le cache pour les URLs du lot
// Préparer les données avec le cache quand disponible
const batchData = batch.map(url => {
if (federatedCache.has(url)) {
return {
url: url,
cached: true,
cachedId: federatedCache.get(url)
};
}
return {
url: url,
cached: false
};
});
// Journaliser les URLs mises en cache
const cachedCount = batchData.filter(item => item.cached).length;
if (cachedCount > 0) {
console.log(`${cachedCount}/${batchData.length} URLs trouvées dans le cache de fédération`, batchData);
// Ajouter une entrée dans le journal si toutes les URLs sont cachées
if (cachedCount === batchData.length) {
addLogEntry(`Traitement accéléré: tous les IDs déjà connus pour ce lot`, 'info');
}
}
console.log('Envoi de la requête avec paramètres:', {
tortoiseMode: isTortoiseMode,
slowMode: isSlowMode,
delaySeconds: delaySeconds
delaySeconds: delaySeconds,
batchWithCache: batchData
});
fetch('process.php', {
@ -944,16 +1243,21 @@ document.addEventListener('DOMContentLoaded', function() {
},
body: JSON.stringify({
batch: batch,
batchData: batchData, // Inclure les données du cache
currentIndex: currentIndex,
totalItems: totalItems,
slowMode: isSlowMode,
tortoiseMode: isTortoiseMode,
delaySeconds: delaySeconds
delaySeconds: delaySeconds,
useCachedIds: true // Indiquer au serveur d'utiliser les IDs en cache quand disponibles
}),
// Augmenter le timeout pour éviter les erreurs de limite de temps
timeout: 60000
})
.then(response => {
// Calculer le temps de réponse
const responseTime = Date.now() - requestStartTime;
// Vérifier si la réponse est au format JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
@ -962,15 +1266,18 @@ document.addEventListener('DOMContentLoaded', function() {
throw new Error(`Réponse non-JSON reçue: ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}`);
});
}
return response.json();
return response.json().then(data => ({ data, responseTime }));
})
.then(data => {
.then(({ data, responseTime }) => {
if (data.success) {
// Traiter les résultats
let hasRateLimitError = false;
let allAlreadyFavorited = true; // Pour AMÉLIORATION 2
if (data.results && data.results.length) {
data.results.forEach(result => {
data.results.forEach((result, index) => {
const url = batch[index];
// Construction du message avec détails si disponibles
let message = result.message;
if (result.details) {
@ -979,6 +1286,29 @@ document.addEventListener('DOMContentLoaded', function() {
addLogEntry(message, result.status);
// AMÉLIORATION 3: Mettre à jour le cache avec les résultats réussis
if (result.status === 'success' && result.misskey_id) {
federatedCache.add(url, result.misskey_id);
}
// AMÉLIORATION 4: Mettre à jour les statistiques de performance API
const domain = extractDomain(url);
let apiSuccess = result.status === 'success' ? true :
(result.status === 'error' || result.status === 'warning') ? false : null;
if (result.error_type === 'rate_limit') {
apiSuccess = 'rate_limit';
}
if (domain) {
apiPerformance.addResponseTime(domain, responseTime, apiSuccess);
}
// AMÉLIORATION 2: Vérifier si c'est déjà favoris pour accélérer le traitement
if (result.status !== 'info' || result.error_type !== 'already_favorited') {
allAlreadyFavorited = false;
}
// Mettre à jour les compteurs
if (result.status === 'success') {
successCount++;
@ -990,16 +1320,27 @@ document.addEventListener('DOMContentLoaded', function() {
} else if (result.status === 'warning') {
warningCount++;
// Détecter les erreurs de rate limit
// AMÉLIORATION 1: Gérer les erreurs de rate limit
if (result.error_type === 'rate_limit') {
hasRateLimitError = true;
// Ajouter à la file d'attente pour réessayer plus tard
if (!rateLimitQueue.includes(url)) {
rateLimitQueue.push(url);
addLogEntry(`URL ajoutée à la file d'attente pour réessai après rate-limit: ${url}`, 'info');
// Sauvegarder la file d'attente
localStorage.setItem('favmastokey_ratelimit_queue', JSON.stringify(rateLimitQueue));
}
}
}
});
}
// Mettre à jour l'index
currentIndex = endIndex;
// Mettre à jour l'index si ce n'est pas un réessai après rate-limit
if (!isRetry) {
currentIndex = Math.min(currentIndex + batch.length, totalItems);
}
// Mettre à jour la progression
updateProgress(data.progress.percentage);
@ -1017,72 +1358,11 @@ document.addEventListener('DOMContentLoaded', function() {
return;
} else if (consecutiveRateLimitErrors >= 2) {
// Si 2 erreurs consécutives, augmenter le délai automatiquement
if (!isTortoiseMode && !isSlowMode) {
// Recommander le mode lent
addLogEntry("Activation automatique du mode ultra-lent suite à des limitations d'API répétées", "warning");
if (slowModeCheckbox) {
slowModeCheckbox.checked = true;
// Mettre à jour l'affichage
if (slowModeWarning) {
slowModeWarning.classList.remove('d-none');
}
if (slowModeOptions) {
slowModeOptions.classList.remove('d-none');
}
// Mettre à jour les options de migration
migration.options.slowMode = true;
isSlowMode = true;
// Sauvegarder dans localStorage
updateMigrationData('in_progress', data.progress);
}
} else if (isSlowMode && !isTortoiseMode) {
// Recommander le mode tortue
addLogEntry("Activation automatique du mode tortue suite à des limitations d'API répétées en mode lent", "warning");
if (tortoiseCheckbox) {
tortoiseCheckbox.checked = true;
// Désactiver le mode lent
if (slowModeCheckbox) {
slowModeCheckbox.checked = false;
if (slowModeWarning) {
slowModeWarning.classList.add('d-none');
}
if (slowModeOptions) {
slowModeOptions.classList.add('d-none');
}
}
// Mettre à jour l'affichage
if (tortoiseWarning) {
tortoiseWarning.classList.remove('d-none');
}
if (tortoiseOptions) {
tortoiseOptions.classList.remove('d-none');
}
// Mettre à jour les options de migration
migration.options.slowMode = false;
migration.options.tortoiseMode = true;
isSlowMode = false;
isTortoiseMode = true;
// Sauvegarder dans localStorage
updateMigrationData('in_progress', data.progress);
}
} else if (isTortoiseMode) {
// Augmenter le délai si possible
increaseAdaptiveDelay();
}
// Attendre plus longtemps avant la prochaine requête
const extraDelayInSec = 30;
addLogEntry(`Ajout d'un délai supplémentaire de ${extraDelayInSec} secondes pour éviter les limitations d'API`, 'info');
// AMÉLIORATION 4: Utiliser les statistiques pour ajuster le délai
adjustDelayBasedOnPerformance();
// Traiter le lot suivant après un délai plus long
setTimeout(processBatch, extraDelayInSec * 1000);
setTimeout(processNextBatch, 30000); // 30 secondes de pause supplémentaire
return;
}
} else {
@ -1105,6 +1385,14 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// AMÉLIORATION 2: Accélérer quand tous les favoris sont déjà traités
if (allAlreadyFavorited) {
// Pas besoin d'attendre le délai complet pour les notes déjà dans les favoris
addLogEntry(`Tous les favoris de ce lot sont déjà présents, traitement immédiat du lot suivant`, 'info');
processNextBatch();
return;
}
// Traiter le lot suivant après un délai adapté au mode
let nextBatchDelay = 3000; // Délai par défaut
@ -1124,7 +1412,7 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Utilisation du délai normal:', nextBatchDelay);
}
setTimeout(processBatch, nextBatchDelay);
setTimeout(processNextBatch, nextBatchDelay);
} else {
// Gérer l'erreur
addLogEntry('Erreur: ' + data.message, 'error');
@ -1171,6 +1459,127 @@ document.addEventListener('DOMContentLoaded', function() {
sendRequest();
}
/**
* Terminer la migration avec succès
*/
function finishMigration() {
isProcessing = false;
// Arrêter les sauvegardes automatiques
saveCheckpoints.stop();
const summary = `Migration terminée ! ${successCount} publications ajoutées aux favoris, ${errorCount} échecs, ${skippedCount} déjà présentes, ${warningCount} avertissements.`;
addLogEntry(summary, 'success');
// Si des URLs sont encore dans la file d'attente rate-limit, avertir l'utilisateur
if (rateLimitQueue.length > 0) {
addLogEntry(`ATTENTION: ${rateLimitQueue.length} favoris n'ont pas pu être traités à cause de limitations d'API. Vous pouvez redémarrer la migration plus tard pour réessayer.`, 'warning');
}
startMigration.classList.remove('d-none');
startMigration.textContent = 'Terminer';
startMigration.addEventListener('click', function() {
// Nettoyer localStorage et retourner à l'étape 1
localStorage.removeItem('favmastokey_favorites');
localStorage.removeItem('favmastokey_migration');
localStorage.removeItem('favmastokey_ratelimit_queue');
// Conserver le cache de fédération et les performances API pour accélérer les futures migrations
federatedCache.cleanup(1440); // Nettoyer les entrées plus vieilles de 24h
federatedCache.saveToStorage();
step3.classList.add('d-none');
step1.classList.remove('d-none');
});
pauseMigration.classList.add('d-none');
// Mettre à jour la progression à 100%
updateProgress(100);
// Mettre à jour le statut de la migration
updateMigrationData('completed');
}
/**
* Ajuste le délai en fonction des performances observées
* AMÉLIORATION 4: Utiliser les statistiques pour ajuster le délai de manière intelligente
*/
function adjustDelayBasedOnPerformance() {
// Identifier les domaines principaux
const domains = [];
// Explorer les URLs récentes pour identifier les domaines
const startIndex = Math.max(0, currentIndex - 10);
const recentUrls = favoritesList.slice(startIndex, currentIndex);
recentUrls.forEach(url => {
const domain = extractDomain(url);
if (domain && !domains.includes(domain)) {
domains.push(domain);
}
});
// Trouver le domaine le plus problématique
let worstDomain = null;
let maxRateLimits = 0;
domains.forEach(domain => {
const stats = apiPerformance.getDomainStats(domain);
if (stats && stats.rateLimitCount > maxRateLimits) {
maxRateLimits = stats.rateLimitCount;
worstDomain = domain;
}
});
if (worstDomain) {
const stats = apiPerformance.getDomainStats(worstDomain);
const recommendedDelay = stats.recommendedDelay;
if (recommendedDelay) {
if (tortoiseCheckbox && tortoiseCheckbox.checked && tortoiseDelay && tortoiseDelayValue) {
// Vérifier si le délai recommandé est supérieur au délai actuel
const currentDelay = parseInt(tortoiseDelay.value);
if (recommendedDelay > currentDelay) {
const newDelay = Math.min(300, recommendedDelay);
tortoiseDelay.value = newDelay;
tortoiseDelayValue.textContent = newDelay;
migration.options.tortoiseDelaySeconds = newDelay;
addLogEntry(`Délai du mode tortue ajusté à ${newDelay}s basé sur les performances avec ${worstDomain}`, 'warning');
}
} else if (slowModeCheckbox && slowModeCheckbox.checked && slowModeDelay && delayValue) {
// Vérifier si le délai recommandé est supérieur au délai actuel
const currentDelay = parseInt(slowModeDelay.value);
if (recommendedDelay > currentDelay) {
const newDelay = Math.min(300, recommendedDelay);
slowModeDelay.value = newDelay;
delayValue.textContent = newDelay;
migration.options.delaySeconds = newDelay;
addLogEntry(`Délai du mode lent ajusté à ${newDelay}s basé sur les performances avec ${worstDomain}`, 'warning');
}
} else {
// Suggérer d'activer le mode lent avec le délai recommandé
addLogEntry(`Activez le mode lent avec un délai d'au moins ${recommendedDelay}s pour éviter les limitations avec ${worstDomain}`, 'warning');
}
}
} else {
// Méthode de secours si aucun domaine problématique identifié
increaseAdaptiveDelay();
}
}
/**
* Extrait le nom de domaine d'une URL
*/
function extractDomain(url) {
try {
const parsedUrl = new URL(url);
return parsedUrl.hostname;
} catch (e) {
console.error("URL invalide:", url);
return null;
}
}
/**
* Met à jour la barre de progression
*/

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,10 +64,13 @@ 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'
'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));
@ -78,10 +81,15 @@ try {
$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);
", delaySeconds=" . $delaySeconds .
", useCachedIds=" . ($useCachedIds ? 'true' : 'false'));
// Ajuster le délai entre les requêtes selon le mode et la valeur personnalisée
if ($tortoiseMode) {
@ -105,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'
@ -114,148 +122,239 @@ 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'];
// 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[] = [
'status' => 'info',
'message' => "Déjà dans vos favoris: $url",
'error_type' => 'already_favorited'
];
} 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);
// 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) {
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');
// 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') {
$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[] = [
'status' => 'warning',
'message' => $message,
'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';
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.";
// 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;
}
}
} 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']));
}
}
// 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");
}
}
// 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é
@ -271,7 +370,8 @@ try {
$suggestions = [
'use_slow_mode' => $hasRateLimitErrors && !$slowMode && !$tortoiseMode,
'use_tortoise_mode' => $hasRateLimitErrors && $slowMode && !$tortoiseMode,
'increase_delay' => $hasRateLimitErrors && ($slowMode || $tortoiseMode) && $delaySeconds < ($tortoiseMode ? 300 : 60)
'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
@ -287,7 +387,8 @@ try {
'config' => [
'slow_mode' => $slowMode,
'tortoise_mode' => $tortoiseMode,
'delay_seconds' => $delaySeconds
'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);
}
}