614 lines
21 KiB
PHP

<?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;
}