initialisation du projet
This commit is contained in:
parent
c43c42a34e
commit
32a61fdb81
72
.htaccess
Normal file
72
.htaccess
Normal file
@ -0,0 +1,72 @@
|
||||
# FavMasToKey - Configuration Apache
|
||||
|
||||
# Activer le moteur de réécriture
|
||||
RewriteEngine On
|
||||
|
||||
# Protéger les fichiers sensibles
|
||||
<FilesMatch "^(config\.php|functions\.php)$">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Bloquer l'accès au répertoire includes
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteRule ^includes/ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# Bloquer l'accès aux fichiers cachés
|
||||
<FilesMatch "^\.">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Forcer HTTPS (décommenter en production)
|
||||
# RewriteCond %{HTTPS} !=on
|
||||
# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# Headers de sécurité
|
||||
<IfModule mod_headers.c>
|
||||
# Empêcher le clickjacking
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
|
||||
# Prévention XSS
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
|
||||
# Empêcher le MIME sniffing
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
|
||||
# Référer policy
|
||||
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Content Security Policy (CSP)
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self';"
|
||||
</IfModule>
|
||||
|
||||
# Désactiver l'affichage du contenu des répertoires
|
||||
Options -Indexes
|
||||
|
||||
# Limiter les méthodes HTTP
|
||||
<LimitExcept GET POST HEAD>
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</LimitExcept>
|
||||
|
||||
# PHP settings
|
||||
<IfModule mod_php.c>
|
||||
# Désactiver l'affichage des erreurs en production
|
||||
# php_flag display_errors Off
|
||||
|
||||
# Limiter le temps d'exécution des scripts
|
||||
php_value max_execution_time 120
|
||||
|
||||
# Limiter la taille des téléchargements
|
||||
php_value upload_max_filesize 10M
|
||||
php_value post_max_size 10M
|
||||
|
||||
# Sécuriser les cookies de session
|
||||
php_value session.cookie_httponly 1
|
||||
php_value session.use_only_cookies 1
|
||||
|
||||
# Utiliser des cookies sécurisés en production
|
||||
# php_value session.cookie_secure 1
|
||||
</IfModule>
|
128
callback.php
Normal file
128
callback.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Callback OAuth pour Misskey
|
||||
*/
|
||||
|
||||
// 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'état est valide (protection CSRF)
|
||||
if (!isset($_GET['state']) || !isset($_SESSION['oauth_state']) || $_GET['state'] !== $_SESSION['oauth_state']) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Paramètre d\'état invalide. Veuillez réessayer.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérifier si le code d'autorisation est présent
|
||||
if (!isset($_GET['code']) || empty($_GET['code'])) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Aucun code d\'autorisation reçu. L\'authentification a échoué ou a été annulée.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer l'instance Misskey depuis la session
|
||||
if (!isset($_SESSION['misskey_instance']) || empty($_SESSION['misskey_instance'])) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Instance Misskey non définie. Veuillez recommencer.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance = $_SESSION['misskey_instance'];
|
||||
$code = $_GET['code'];
|
||||
|
||||
// En production, ici nous échangerions le code contre un token d'accès
|
||||
// Pour cette version initiale, nous simulons l'échange
|
||||
|
||||
// Simulation : générer un token fictif
|
||||
// Dans une implémentation réelle, nous appellerions l'API Misskey pour obtenir un vrai token
|
||||
$_SESSION['misskey_token'] = 'DEMO_' . bin2hex(random_bytes(16));
|
||||
|
||||
// Dans un cas réel, nous aurions un code similaire à celui-ci:
|
||||
/*
|
||||
// Construire l'URL pour l'échange du code
|
||||
$token_url = "https://{$instance}/oauth/token";
|
||||
|
||||
// Paramètres de la requête
|
||||
$params = [
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $config['app_url'] . '/callback.php'
|
||||
];
|
||||
|
||||
// Initialiser cURL
|
||||
$ch = curl_init();
|
||||
|
||||
// Configurer la requête
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $token_url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($params),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/x-www-form-urlencoded'
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
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);
|
||||
|
||||
// Fermer la session cURL
|
||||
curl_close($ch);
|
||||
|
||||
// Vérifier les erreurs
|
||||
if ($error || $http_code !== 200) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Erreur lors de l\'échange du code d\'autorisation: ' . ($error ?: 'HTTP ' . $http_code)
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Décoder la réponse
|
||||
$response_data = json_decode($response, true);
|
||||
|
||||
// Vérifier que le token est présent
|
||||
if (!isset($response_data['access_token']) || empty($response_data['access_token'])) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Aucun token d\'accès reçu. L\'authentification a échoué.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Stocker le token dans la session
|
||||
$_SESSION['misskey_token'] = $response_data['access_token'];
|
||||
*/
|
||||
|
||||
// Nettoyer l'état OAuth
|
||||
unset($_SESSION['oauth_state']);
|
||||
|
||||
// Ajouter un message de succès
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'success',
|
||||
'text' => 'Connecté avec succès à ' . $instance . '.'
|
||||
];
|
||||
|
||||
// Rediriger vers la page de migration (étape 3)
|
||||
header('Location: index.php#step3');
|
89
css/styles.css
Normal file
89
css/styles.css
Normal file
@ -0,0 +1,89 @@
|
||||
/* FavMasToKey - Styles personnalisés */
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #563d7c;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #563d7c;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#log-container {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#operation-log .log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#operation-log .success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
#operation-log .error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
#operation-log .info {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
#operation-log .warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
/* Animation pour montrer le progrès */
|
||||
@keyframes progress-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.progress-bar.active {
|
||||
animation: progress-pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Styles pour les boutons */
|
||||
.btn-primary {
|
||||
background-color: #563d7c;
|
||||
border-color: #563d7c;
|
||||
}
|
||||
|
||||
.btn-primary:hover, .btn-primary:focus {
|
||||
background-color: #452d6b;
|
||||
border-color: #452d6b;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
56
includes/config.php
Normal file
56
includes/config.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Configuration
|
||||
*/
|
||||
|
||||
// Empêcher l'accès direct au fichier
|
||||
if (!defined('FAVMASTOKEY')) {
|
||||
die('Accès direct interdit');
|
||||
}
|
||||
|
||||
// Environnement (development ou production)
|
||||
define('ENVIRONMENT', 'development');
|
||||
|
||||
// Gestion des erreurs selon l'environnement
|
||||
if (ENVIRONMENT === 'development') {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
} else {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
}
|
||||
|
||||
// Configuration de l'application
|
||||
$config = [
|
||||
// Informations de l'application (à remplir lors de la création de l'app sur Misskey)
|
||||
'app_name' => 'FavMasToKey',
|
||||
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
|
||||
'app_version' => '0.1.0',
|
||||
|
||||
// Paramètres OAuth - À CONFIGURER
|
||||
'client_id' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey
|
||||
'client_secret' => '', // Obtenus lors de l'enregistrement de votre app sur Misskey
|
||||
|
||||
// URLs de base
|
||||
'app_url' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
|
||||
|
||||
// Paramètres Misskey API
|
||||
'misskey_api_endpoint' => '/api/notes/favorites/create',
|
||||
|
||||
// Paramètres pour le traitement
|
||||
'batch_size' => 10, // Nombre de favoris à traiter en une fois
|
||||
'timeout' => 30, // Timeout des requêtes en secondes
|
||||
'max_retries' => 3 // Nombre maximal de tentatives par favori
|
||||
];
|
||||
|
||||
// Session
|
||||
session_start();
|
||||
|
||||
// Fonctions utilitaires
|
||||
function debug($data) {
|
||||
if (ENVIRONMENT === 'development') {
|
||||
echo '<pre>';
|
||||
print_r($data);
|
||||
echo '</pre>';
|
||||
}
|
||||
}
|
199
includes/functions.php
Normal file
199
includes/functions.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Fonctions utilitaires
|
||||
*/
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array Résultat de la requête
|
||||
*/
|
||||
function misskey_api_request($instance, $endpoint, $data, $token) {
|
||||
global $config;
|
||||
|
||||
// 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);
|
||||
|
||||
// Fermer la session cURL
|
||||
curl_close($ch);
|
||||
|
||||
// Vérifier les erreurs
|
||||
if ($error) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Erreur cURL: ' . $error,
|
||||
'http_code' => $http_code
|
||||
];
|
||||
}
|
||||
|
||||
// Décoder la réponse
|
||||
$response_data = json_decode($response, true);
|
||||
|
||||
// Vérifier si la requête a réussi
|
||||
if ($http_code >= 200 && $http_code < 300) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response_data,
|
||||
'http_code' => $http_code
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => isset($response_data['error']) ? $response_data['error'] : 'Erreur API Misskey',
|
||||
'http_code' => $http_code,
|
||||
'data' => $response_data
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une URL d'autorisation OAuth pour Misskey
|
||||
*
|
||||
* @param string $instance Instance Misskey
|
||||
* @return string URL d'autorisation
|
||||
*/
|
||||
function generate_oauth_url($instance) {
|
||||
global $config;
|
||||
|
||||
// Générer un état aléatoire pour la sécurité
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$_SESSION['oauth_state'] = $state;
|
||||
$_SESSION['misskey_instance'] = $instance;
|
||||
|
||||
// Construire l'URL de callback
|
||||
$callback_url = $config['app_url'] . '/callback.php';
|
||||
|
||||
// Paramètres de la requête d'autorisation
|
||||
$params = [
|
||||
'client_id' => $config['client_id'],
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $callback_url,
|
||||
'scope' => 'write:favorites',
|
||||
'state' => $state
|
||||
];
|
||||
|
||||
// Construire l'URL d'autorisation
|
||||
$auth_url = "https://{$instance}/oauth/authorize?" . http_build_query($params);
|
||||
|
||||
return $auth_url;
|
||||
}
|
151
index.php
Normal file
151
index.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Page d'accueil
|
||||
*/
|
||||
|
||||
// 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é
|
||||
$is_authenticated = isset($_SESSION['misskey_token']) && !empty($_SESSION['misskey_token']);
|
||||
$instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
|
||||
|
||||
// Initialiser les messages
|
||||
$messages = [];
|
||||
if (isset($_SESSION['messages'])) {
|
||||
$messages = $_SESSION['messages'];
|
||||
unset($_SESSION['messages']);
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FavMasToKey - Transférer vos favoris de Mastodon vers Misskey</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">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<header class="text-center mb-5">
|
||||
<h1>FavMasToKey</h1>
|
||||
<p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p>
|
||||
</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']; ?>
|
||||
<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-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="steps">
|
||||
<!-- Étape 1: Téléchargement du fichier JSON -->
|
||||
<div class="step" id="step1">
|
||||
<h3 class="card-title">1. Importer vos favoris Mastodon</h3>
|
||||
<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>
|
||||
|
||||
<!-- Étape 2: Connexion à Misskey -->
|
||||
<div class="step d-none" id="step2">
|
||||
<h3 class="card-title">2. Connexion à Misskey</h3>
|
||||
<p>Connectez-vous à votre compte Misskey pour y importer vos favoris.</p>
|
||||
|
||||
<div id="file-summary" class="alert alert-info mb-4"></div>
|
||||
|
||||
<form id="misskey-form" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="misskey-instance" class="form-label">Instance Misskey</label>
|
||||
<input type="url" class="form-control" id="misskey-instance" name="misskey_instance"
|
||||
placeholder="https://misskey.io" required>
|
||||
<div class="form-text">Entrez l'URL complète de votre instance Misskey (ex: https://misskey.io)</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Se connecter à Misskey</button>
|
||||
<button type="button" class="btn btn-link" id="back-to-step1">Retour</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Étape 3: Migration des favoris -->
|
||||
<div class="step d-none" id="step3">
|
||||
<h3 class="card-title">3. Migration des favoris</h3>
|
||||
<p>Nous allons maintenant transférer vos favoris vers Misskey.</p>
|
||||
|
||||
<?php if ($is_authenticated): ?>
|
||||
<div class="alert alert-success mb-4">
|
||||
<strong>Connecté à <?php echo htmlspecialchars($instance); ?></strong>
|
||||
<p class="mb-0">Vous êtes authentifié et prêt à importer vos favoris.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<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-4">
|
||||
<label class="form-label">Opération en cours</label>
|
||||
<div class="progress">
|
||||
<div id="current-progress" class="progress-bar bg-info" role="progressbar"
|
||||
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5>Journal des opérations</h5>
|
||||
<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 class="d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-primary" id="start-migration">Démarrer la migration</button>
|
||||
<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>
|
||||
</div>
|
||||
</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/app.js"></script>
|
||||
</body>
|
||||
</html>
|
323
js/app.js
Normal file
323
js/app.js
Normal file
@ -0,0 +1,323 @@
|
||||
/**
|
||||
* FavMasToKey - Script JavaScript principal
|
||||
*/
|
||||
|
||||
// Attendre que le DOM soit chargé
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Éléments DOM
|
||||
const uploadForm = document.getElementById('upload-form');
|
||||
const misskeyForm = document.getElementById('misskey-form');
|
||||
const jsonFileInput = document.getElementById('json-file');
|
||||
const step1 = document.getElementById('step1');
|
||||
const step2 = document.getElementById('step2');
|
||||
const step3 = document.getElementById('step3');
|
||||
const fileSummary = document.getElementById('file-summary');
|
||||
const backToStep1 = document.getElementById('back-to-step1');
|
||||
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 currentProgress = document.getElementById('current-progress');
|
||||
const operationLog = document.getElementById('operation-log');
|
||||
|
||||
// Variables globales
|
||||
let favoritesList = [];
|
||||
let currentIndex = 0;
|
||||
let totalItems = 0;
|
||||
let isProcessing = false;
|
||||
let isPaused = false;
|
||||
|
||||
// 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.innerHTML = `
|
||||
<strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
|
||||
`;
|
||||
|
||||
// Passer à l'étape 2
|
||||
step1.classList.add('d-none');
|
||||
step2.classList.remove('d-none');
|
||||
|
||||
// Stocker les données dans localStorage pour les conserver
|
||||
localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList));
|
||||
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
|
||||
// Gestion du retour à l'étape 1
|
||||
if (backToStep1) {
|
||||
backToStep1.addEventListener('click', function() {
|
||||
step2.classList.add('d-none');
|
||||
step1.classList.remove('d-none');
|
||||
});
|
||||
}
|
||||
|
||||
// Gérer la connexion à Misskey
|
||||
if (misskeyForm) {
|
||||
misskeyForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const instanceInput = document.getElementById('misskey-instance');
|
||||
let instance = instanceInput.value.trim();
|
||||
|
||||
// Vérifier que l'instance est valide
|
||||
if (!instance) {
|
||||
alert('Veuillez entrer l\'URL de votre instance Misskey.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Supprimer le protocole et les slash de fin si présents
|
||||
instance = instance.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Vérifier que l'URL semble valide
|
||||
if (!/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(instance)) {
|
||||
alert('L\'URL de l\'instance Misskey semble invalide.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Rediriger vers l'authentification OAuth
|
||||
window.location.href = `oauth.php?instance=${encodeURIComponent(instance)}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Gérer le processus de migration
|
||||
if (startMigration) {
|
||||
startMigration.addEventListener('click', function() {
|
||||
if (isProcessing) return;
|
||||
|
||||
// Récupérer les favoris depuis 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;
|
||||
}
|
||||
|
||||
// Démarrer la migration
|
||||
isProcessing = true;
|
||||
isPaused = false;
|
||||
startMigration.classList.add('d-none');
|
||||
pauseMigration.classList.remove('d-none');
|
||||
|
||||
addLogEntry('Démarrage de la migration...', 'info');
|
||||
|
||||
// Lancer le processus de migration
|
||||
processBatch();
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
currentProgress.classList.remove('active');
|
||||
} else {
|
||||
pauseMigration.textContent = 'Pause';
|
||||
addLogEntry('Reprise de la migration...', 'info');
|
||||
currentProgress.classList.add('active');
|
||||
processBatch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Gérer l'annulation de la migration
|
||||
if (cancelMigration) {
|
||||
cancelMigration.addEventListener('click', function() {
|
||||
if (!isProcessing && currentIndex === 0) {
|
||||
// Retour à l'étape 1 si rien n'a commencé
|
||||
step3.classList.add('d-none');
|
||||
step1.classList.remove('d-none');
|
||||
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');
|
||||
|
||||
// Réinitialiser l'interface
|
||||
startMigration.classList.remove('d-none');
|
||||
pauseMigration.classList.add('d-none');
|
||||
pauseMigration.textContent = 'Pause';
|
||||
currentProgress.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite un lot de favoris
|
||||
*/
|
||||
function processBatch() {
|
||||
if (!isProcessing || isPaused || currentIndex >= totalItems) {
|
||||
if (currentIndex >= totalItems) {
|
||||
// Migration terminée
|
||||
isProcessing = false;
|
||||
addLogEntry('Migration terminée avec succès !', 'success');
|
||||
startMigration.classList.remove('d-none');
|
||||
startMigration.textContent = 'Terminer';
|
||||
startMigration.addEventListener('click', function() {
|
||||
// Nettoyer localStorage et retourner à l'étape 1
|
||||
localStorage.removeItem('favmastokey_favorites');
|
||||
step3.classList.add('d-none');
|
||||
step1.classList.remove('d-none');
|
||||
});
|
||||
pauseMigration.classList.add('d-none');
|
||||
|
||||
// Mettre à jour la progression à 100%
|
||||
updateProgress(100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Nombre d'éléments à traiter dans ce lot
|
||||
const batchSize = 5;
|
||||
const endIndex = Math.min(currentIndex + batchSize, totalItems);
|
||||
|
||||
// Préparer les éléments du lot
|
||||
const batch = favoritesList.slice(currentIndex, endIndex);
|
||||
|
||||
// Mettre à jour la progression actuelle
|
||||
currentProgress.classList.add('active');
|
||||
updateProgress();
|
||||
|
||||
// Simuler le traitement (à remplacer par l'appel API réel)
|
||||
addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info');
|
||||
|
||||
// Envoyer la requête au serveur
|
||||
fetch('process.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
batch: batch,
|
||||
currentIndex: currentIndex,
|
||||
totalItems: totalItems
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Traiter les résultats
|
||||
if (data.results && data.results.length) {
|
||||
data.results.forEach(result => {
|
||||
addLogEntry(result.message, result.status);
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour l'index
|
||||
currentIndex = endIndex;
|
||||
|
||||
// Mettre à jour la progression
|
||||
updateProgress();
|
||||
|
||||
// Traiter le lot suivant après un court délai
|
||||
setTimeout(processBatch, 1000);
|
||||
} else {
|
||||
// Gérer l'erreur
|
||||
addLogEntry('Erreur: ' + data.message, 'error');
|
||||
|
||||
// Pause en cas d'erreur
|
||||
isPaused = true;
|
||||
pauseMigration.textContent = 'Reprendre';
|
||||
currentProgress.classList.remove('active');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Gérer l'erreur réseau
|
||||
addLogEntry('Erreur de connexion: ' + error.message, 'error');
|
||||
|
||||
// Pause en cas d'erreur
|
||||
isPaused = true;
|
||||
pauseMigration.textContent = 'Reprendre';
|
||||
currentProgress.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la barre de progression
|
||||
*/
|
||||
function updateProgress(forcedValue = null) {
|
||||
const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100;
|
||||
globalProgress.style.width = progress + '%';
|
||||
globalProgress.textContent = Math.round(progress) + '%';
|
||||
globalProgress.setAttribute('aria-valuenow', progress);
|
||||
|
||||
if (forcedValue === null) {
|
||||
// Mettre à jour la progression actuelle
|
||||
const batchProgress = ((currentIndex % 5) / 5) * 100;
|
||||
currentProgress.style.width = batchProgress + '%';
|
||||
currentProgress.setAttribute('aria-valuenow', batchProgress);
|
||||
} else {
|
||||
currentProgress.style.width = '100%';
|
||||
currentProgress.setAttribute('aria-valuenow', 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une entrée dans le journal des opérations
|
||||
*/
|
||||
function addLogEntry(message, status = 'info') {
|
||||
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');
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
});
|
82
oauth.php
Normal file
82
oauth.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Authentification OAuth avec Misskey
|
||||
*/
|
||||
|
||||
// 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'instance est fournie
|
||||
if (!isset($_GET['instance']) || empty($_GET['instance'])) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Aucune instance Misskey spécifiée.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer l'instance
|
||||
$instance = trim($_GET['instance']);
|
||||
|
||||
// Supprimer le protocole et les slash de fin si présents
|
||||
$instance = preg_replace('/^https?:\/\//', '', $instance);
|
||||
$instance = rtrim($instance, '/');
|
||||
|
||||
// Vérifier que l'instance semble valide
|
||||
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.'
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Stocker l'instance dans la session
|
||||
$_SESSION['misskey_instance'] = $instance;
|
||||
|
||||
// En production, ici nous devrions vérifier si l'application est déjà enregistrée sur cette instance
|
||||
// Si ce n'est pas le cas, il faudrait l'enregistrer via l'API Misskey
|
||||
// Pour cette version initiale, nous utilisons des valeurs simulées
|
||||
|
||||
// Générer l'URL d'autorisation
|
||||
try {
|
||||
// REMARQUE: Pour une implémentation réelle, $config['client_id'] et $config['client_secret']
|
||||
// devraient être stockés par instance car chaque instance Misskey nécessite une application distincte
|
||||
|
||||
// Générer un état aléatoire pour la sécurité
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$_SESSION['oauth_state'] = $state;
|
||||
|
||||
// Construire l'URL de callback (doit correspondre à celle configurée dans l'application Misskey)
|
||||
$callback_url = $config['app_url'] . '/callback.php';
|
||||
|
||||
// Paramètres de la requête d'autorisation
|
||||
$params = [
|
||||
'client_id' => $config['client_id'] ?: 'DEMO_CLIENT_ID', // En production, utiliser une valeur réelle
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $callback_url,
|
||||
'scope' => 'write:favorites',
|
||||
'state' => $state
|
||||
];
|
||||
|
||||
// Construire l'URL d'autorisation
|
||||
$auth_url = "https://{$instance}/oauth/authorize?" . http_build_query($params);
|
||||
|
||||
// Rediriger vers l'URL d'autorisation
|
||||
header('Location: ' . $auth_url);
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Erreur lors de la préparation de l\'authentification: ' . $e->getMessage()
|
||||
];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
156
process.php
Normal file
156
process.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Traitement des favoris
|
||||
*/
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est authentifié
|
||||
if (!isset($_SESSION['misskey_token']) || empty($_SESSION['misskey_token'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Non authentifié']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer l'instance Misskey
|
||||
$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 le token d'accès
|
||||
$token = $_SESSION['misskey_token'];
|
||||
|
||||
// Récupérer les données envoyées
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$input || !isset($input['batch']) || !is_array($input['batch'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Données invalides']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer le lot à traiter
|
||||
$batch = $input['batch'];
|
||||
$currentIndex = isset($input['currentIndex']) ? (int)$input['currentIndex'] : 0;
|
||||
$totalItems = isset($input['totalItems']) ? (int)$input['totalItems'] : count($batch);
|
||||
|
||||
// Résultats du traitement
|
||||
$results = [];
|
||||
|
||||
// Traiter chaque URL du lot
|
||||
foreach ($batch as $index => $url) {
|
||||
// Extraire les informations de l'URL
|
||||
$urlParts = parse_url($url);
|
||||
|
||||
// Vérifier que l'URL est valide
|
||||
if (!$urlParts || !isset($urlParts['host']) || !isset($urlParts['path'])) {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "URL invalide: $url"
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extraire l'identifiant du toot
|
||||
$pathParts = explode('/', trim($urlParts['path'], '/'));
|
||||
$tootId = end($pathParts);
|
||||
|
||||
if (!is_numeric($tootId)) {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Impossible d'extraire l'ID du toot: $url"
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construire l'URL pour la recherche fédérée sur Misskey
|
||||
$searchUrl = "https://" . $urlParts['host'] . "/@" . $pathParts[count($pathParts) - 3] . "/" . $tootId;
|
||||
|
||||
// En production, ici nous ferions une recherche sur Misskey pour trouver l'équivalent du toot
|
||||
// Pour cette version initiale, nous simulons la réussite/échec
|
||||
|
||||
// Simulation : 90% de réussite, 10% d'échec
|
||||
$success = (rand(1, 10) <= 9);
|
||||
|
||||
if ($success) {
|
||||
// Simulation de l'ajout aux favoris
|
||||
$results[] = [
|
||||
'status' => 'success',
|
||||
'message' => "Ajouté aux favoris: $url"
|
||||
];
|
||||
} else {
|
||||
// Simulation d'erreur
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Impossible d'ajouter aux favoris: $url (publication introuvable ou inaccessible)"
|
||||
];
|
||||
}
|
||||
|
||||
// Dans un cas réel, nous aurions un code similaire à celui-ci:
|
||||
/*
|
||||
// Rechercher la note sur Misskey
|
||||
$searchData = [
|
||||
'query' => $searchUrl,
|
||||
'limit' => 1
|
||||
];
|
||||
|
||||
$searchResult = misskey_api_request($misskey_instance, '/api/notes/search', $searchData, $token);
|
||||
|
||||
if ($searchResult['success'] && !empty($searchResult['data'])) {
|
||||
// Récupérer l'ID de la note trouvée
|
||||
$noteId = $searchResult['data'][0]['id'];
|
||||
|
||||
// Ajouter aux favoris
|
||||
$favoriteData = [
|
||||
'noteId' => $noteId
|
||||
];
|
||||
|
||||
$favoriteResult = misskey_api_request($misskey_instance, '/api/notes/favorites/create', $favoriteData, $token);
|
||||
|
||||
if ($favoriteResult['success']) {
|
||||
$results[] = [
|
||||
'status' => 'success',
|
||||
'message' => "Ajouté aux favoris: $url"
|
||||
];
|
||||
} else {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Erreur lors de l'ajout aux favoris: " . $favoriteResult['message']
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Publication introuvable sur Misskey: $url"
|
||||
];
|
||||
}
|
||||
*/
|
||||
|
||||
// Pause pour éviter de surcharger l'API (à utiliser en production)
|
||||
// usleep(200000); // 200 ms
|
||||
}
|
||||
|
||||
// Renvoyer les résultats
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'results' => $results,
|
||||
'progress' => [
|
||||
'current' => $currentIndex + count($batch),
|
||||
'total' => $totalItems,
|
||||
'percentage' => round((($currentIndex + count($batch)) / $totalItems) * 100, 2)
|
||||
]
|
||||
]);
|
Loading…
x
Reference in New Issue
Block a user