<?php /** * FavMasToKey - Fonctions utilitaires (version améliorée) */ // Empêcher l'accès direct au fichier if (!defined('FAVMASTOKEY')) { die('Accès direct interdit'); } /** * Valide un fichier JSON de favoris Mastodon * * @param string $file_path Chemin vers le fichier JSON * @return array|bool Tableau contenant les données du fichier ou false en cas d'erreur */ function validate_mastodon_json($file_path) { // Vérifier si le fichier existe if (!file_exists($file_path)) { return [ 'success' => false, 'message' => 'Le fichier n\'existe pas.' ]; } // Lire le contenu du fichier $content = file_get_contents($file_path); if (!$content) { return [ 'success' => false, 'message' => 'Impossible de lire le contenu du fichier.' ]; } // Décoder le JSON $json = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { return [ 'success' => false, 'message' => 'Le fichier n\'est pas un JSON valide: ' . json_last_error_msg() ]; } // Vérifier la structure du fichier if (!isset($json['@context']) || !isset($json['type']) || !isset($json['orderedItems'])) { return [ 'success' => false, 'message' => 'Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.' ]; } // Vérifier que orderedItems est un tableau if (!is_array($json['orderedItems'])) { return [ 'success' => false, 'message' => 'Le format des favoris dans le fichier est invalide.' ]; } // Tout est OK return [ 'success' => true, 'data' => $json, 'count' => count($json['orderedItems']) ]; } /** * Extrait les identifiants de publications à partir des URLs Mastodon * * @param array $urls Tableau d'URLs Mastodon * @return array Tableau d'identifiants extraits */ function extract_toot_ids($urls) { $ids = []; foreach ($urls as $url) { // Format attendu: https://instance.tld/users/username/statuses/id $parts = explode('/', $url); // L'ID devrait être le dernier élément après "statuses" $id = end($parts); if (is_numeric($id)) { $ids[] = [ 'original_url' => $url, 'toot_id' => $id, 'instance' => parse_url($url, PHP_URL_HOST), 'username' => isset($parts[count($parts) - 3]) ? $parts[count($parts) - 3] : null ]; } } 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 * * @param string $instance Instance Misskey (ex: misskey.io) * @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, $with_timing = false) { global $config; $start_time = microtime(true); // Construire l'URL complète $url = "https://{$instance}{$endpoint}"; // Ajouter le token d'accès aux données $data['i'] = $token; // Initialiser cURL $ch = curl_init(); // Configurer la requête curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'User-Agent: FavMasToKey/' . $config['app_version'] ], CURLOPT_TIMEOUT => $config['timeout'], CURLOPT_SSL_VERIFYPEER => true ]); // Exécuter la requête $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); // 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) { $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 $response_data = json_decode($response, true); // Analyse spécifique des erreurs if ($http_code == 429) { $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) { $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'; $error_message = 'Erreur API Misskey'; if (isset($response_data['error'])) { if (isset($response_data['error']['code'])) { $error_code = $response_data['error']['code']; } if (isset($response_data['error']['message'])) { $error_message = $response_data['error']['message']; } } $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; } } /** * Valide un jeton d'accès Misskey en effectuant une requête test * * @param string $instance Instance Misskey * @param string $token Jeton d'accès à valider * @return array Résultat de la validation */ function validate_misskey_token($instance, $token) { // Test basique de connexion avec un simple ping $ping_result = misskey_api_request($instance, '/api/ping', [], $token); if (!$ping_result['success']) { return $ping_result; } // Test de récupération d'informations du compte actuel (permission la plus basique) $account_test = misskey_api_request($instance, '/api/i', [], $token); if (!$account_test['success']) { return [ 'success' => false, 'message' => 'Le jeton est valide mais n\'a pas accès aux informations du compte. Assurez-vous d\'avoir accordé la permission "Afficher les informations du compte".', 'data' => $account_test['data'], 'error_code' => 'INSUFFICIENT_PERMISSIONS' ]; } // Test de validation pour vérifier la permission d'ajouter aux favoris // On n'a pas besoin d'ajouter un favori réel, juste de vérifier si on peut voir les favoris $favorites_test = misskey_api_request($instance, '/api/i/favorites', ['limit' => 1], $token); if (!$favorites_test['success']) { return [ 'success' => false, 'message' => 'Le jeton est valide mais n\'a pas accès aux favoris. Assurez-vous d\'avoir accordé les permissions "Afficher les favoris" et "Gérer les favoris".', 'data' => $favorites_test['data'], 'error_code' => 'INSUFFICIENT_PERMISSIONS' ]; } // Tout est bon return [ 'success' => true, 'message' => 'Jeton valide avec toutes les permissions nécessaires', 'data' => [ 'account' => isset($account_test['data']['username']) ? $account_test['data']['username'] : 'Compte validé', 'ping' => $ping_result['data'] ] ]; } /** * Vérifie si une note est déjà dans les favoris * * @param string $instance Instance Misskey * @param string $note_id ID de la note à vérifier * @param string $token Token d'accès * @return array Résultat de la vérification */ function check_if_favorited($instance, $note_id, $token) { // Endpoint pour récupérer le statut de favori d'une note $endpoint = '/api/notes/show'; // Données pour la requête $data = [ 'noteId' => $note_id ]; // Effectuer la requête avec timing pour les statistiques de performance $result = misskey_api_request($instance, $endpoint, $data, $token, true); if ($result['success'] && isset($result['data'])) { // La note existe et nous avons une réponse // Vérifier si l'utilisateur a déjà mis cette note en favori if (isset($result['data']['isFavorited']) && $result['data']['isFavorited'] === true) { return [ 'success' => true, 'is_favorited' => true, 'message' => 'Cette note est déjà dans vos favoris', 'timing' => isset($result['timing']) ? $result['timing'] : null ]; } else { return [ 'success' => true, 'is_favorited' => false, 'message' => 'Cette note n\'est pas encore dans vos favoris', 'timing' => isset($result['timing']) ? $result['timing'] : null ]; } } // En cas d'erreur ou si la note n'existe pas return [ 'success' => false, 'is_favorited' => false, 'message' => isset($result['message']) ? $result['message'] : 'Impossible de vérifier le statut de favori', 'error_code' => isset($result['error_code']) ? $result['error_code'] : 'UNKNOWN_ERROR', 'timing' => isset($result['timing']) ? $result['timing'] : null ]; } /** * Recherche une note Mastodon sur le réseau fédéré de Misskey * Utilise l'endpoint ap/show qui s'est avéré le plus fiable avec différentes instances * * @param string $instance Instance Misskey * @param string $url URL de la publication Mastodon * @param string $token Token d'accès * @return array Résultat de la recherche */ function search_federated_note($instance, $url, $token) { // Nettoyer l'URL (enlever les éventuels paramètres) $cleanUrl = strtok($url, '?'); // Journal de débogage error_log("Recherche fédérée pour: " . $cleanUrl); // Méthode principale: Utiliser ap/show qui fonctionne avec la plupart des instances $endpoint = '/api/ap/show'; $data = [ 'uri' => $cleanUrl ]; // 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'])) { error_log("Format de réponse ap/show: " . json_encode(array_keys($result['data']))); } // Si la méthode principale a réussi et renvoie un ID, retourner le résultat if ($result['success'] && isset($result['data'])) { // Vérifier si l'ID existe directement if (isset($result['data']['id'])) { return $result; } // Certaines instances peuvent avoir l'ID dans 'note' if (isset($result['data']['note']) && isset($result['data']['note']['id'])) { // Remonter l'ID au niveau principal $result['data']['id'] = $result['data']['note']['id']; return $result; } // Pour les instances plus récentes qui utilisent un format différent if (!empty($result['data'])) { // Rechercher un champ qui pourrait contenir l'ID foreach (['id', 'noteId', 'objectId', 'originalId'] as $possibleIdField) { if (isset($result['data'][$possibleIdField])) { $result['data']['id'] = $result['data'][$possibleIdField]; return $result; } } // Si toujours pas d'ID, examiner la structure pour le trouver foreach ($result['data'] as $key => $value) { if (is_array($value) && isset($value['id'])) { $result['data']['id'] = $value['id']; return $result; } } } } // Vérifier si c'est une erreur de rate limiting if (!$result['success'] && isset($result['error_code']) && $result['error_code'] === 'RATE_LIMIT_EXCEEDED') { return $result; } // 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, 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']))); // Vérifier si nous avons un résultat avec un ID if (isset($fallback_result['data']['id'])) { return $fallback_result; } // Si le résultat est un tableau (certaines instances renvoient un tableau) if (is_array($fallback_result['data']) && !isset($fallback_result['data']['id'])) { // Chercher le premier élément avec un ID foreach ($fallback_result['data'] as $item) { if (is_array($item) && isset($item['id'])) { $fallback_result['data'] = $item; // Utiliser cet élément comme résultat return $fallback_result; } } } } // Vérifier à nouveau si c'est une erreur de rate limiting if (!$fallback_result['success'] && isset($fallback_result['error_code']) && $fallback_result['error_code'] === 'RATE_LIMIT_EXCEEDED') { return $fallback_result; } // Méthode de secours 2: Extraction et recherche par ID distant $urlParts = parse_url($cleanUrl); if (isset($urlParts['path'])) { $pathParts = explode('/', trim($urlParts['path'], '/')); if (count($pathParts) >= 4 && $pathParts[count($pathParts) - 2] === 'statuses') { $statusId = end($pathParts); $username = $pathParts[count($pathParts) - 3]; $acctDomain = isset($urlParts['host']) ? $urlParts['host'] : ''; if ($statusId && $username && $acctDomain) { $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, true); if ($remote_result['success'] && isset($remote_result['data']['id'])) { return $remote_result; } // Vérifier si c'est une erreur de rate limiting if (!$remote_result['success'] && isset($remote_result['error_code']) && $remote_result['error_code'] === 'RATE_LIMIT_EXCEEDED') { return $remote_result; } // Dernier recours: essayer renotes/search $renote_result = misskey_api_request($instance, '/api/notes/search', [ 'query' => "@{$username}@{$acctDomain} {$statusId}", 'limit' => 10 ], $token, true); if ($renote_result['success'] && !empty($renote_result['data'])) { // Parcourir les résultats pour trouver une correspondance foreach ($renote_result['data'] as $note) { if (isset($note['id'])) { $renote_result['data'] = $note; return $renote_result; } } } // Vérifier si c'est une erreur de rate limiting if (!$renote_result['success'] && isset($renote_result['error_code']) && $renote_result['error_code'] === 'RATE_LIMIT_EXCEEDED') { return $renote_result; } } } } // Si aucune méthode n'a fonctionné, retourner une erreur return [ '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, 'timing' => isset($result['timing']) ? $result['timing'] : null ]; } /** * Ajoute une note aux favoris sur Misskey * * @param string $instance Instance Misskey * @param string $note_id ID de la note à ajouter aux favoris * @param string $token Token d'accès * @return array Résultat de l'opération */ function add_to_favorites($instance, $note_id, $token) { global $config; // Endpoint pour ajouter aux favoris $endpoint = $config['misskey_api_endpoint']; // Données pour l'ajout aux favoris $data = [ 'noteId' => $note_id ]; // Effectuer la requête 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); 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; }