614 lines
21 KiB
PHP
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;
|
|
} |