Aller au contenu
Suivre @BlockFarRight sur X (nouvel onglet)

Outils de blocage

Ces outils bloquent, depuis ton compte X, Instagram, Facebook ou TikTok et quand tu les lances, tous les comptes de la liste pour la plateforme choisie. Ils tournent uniquement dans ton navigateur, sur la plateforme où tu es connecté. Au lancement, l’outil ouvre une petite fenêtre de ce site, qui lui transmet la liste à jour ; ensuite, il n’échange qu’avec la plateforme, avec ta session.

En ce moment, la liste compte 2 841 comptes pour ces outils, mis à jour en direct.

Avant de commencer

  • C’est ta décision, et ton compte. Un outil fait, à ta place et à ton rythme, ce que tu pourrais faire à la main : bloquer des comptes depuis ton compte. C’est toi qui le lances, et tu peux l’arrêter à tout moment.
  • Les plateformes peuvent l’interdire. Leurs conditions d’utilisation interdisent ou limitent souvent l’automatisation. Utiliser un outil peut donc les enfreindre, et la plateforme peut restreindre, suspendre ou fermer ton compte. Lis ses conditions avant de lancer un outil. Si tu préfères ne prendre aucun risque, bloque à la main avec la page Un par un.
  • Sans garantie. Les outils sont gratuits et fournis tels quels. Dans les limites permises par la loi, l’éditeur ne répond pas des décisions des plateformes à l’égard de ton compte.
  • Ne colle dans une console que le code publié sur cette page. Un code reçu par message, dans une vidéo ou d’une autre personne peut servir à voler ton compte.
  • Lis le code avant de le lancer : il est commenté et lisible, et son empreinte SHA-256 est indiquée. Il ne contient pas la liste : il la reçoit au lancement, toujours à jour, si bien que la même copie sert longtemps.
  • Les plateformes limitent les actions répétées. Chaque outil fait une pause de quelques secondes entre deux blocages (la durée est sur sa fiche) et s’arrête après 40 comptes par lancement ; relance-le plus tard pour continuer, il reprend là où il s’est arrêté. Une plateforme peut quand même restreindre un compte qui bloque beaucoup en peu de temps.
  • Les outils ont été vérifiés sur des pages simulées, pas sur les vraies plateformes, qui peuvent changer leur fonctionnement à tout moment. Si un outil s’arrête avec un message d’erreur, signale-le.

Comment lancer un outil

Console

Ouvre la plateforme dans ton navigateur d’ordinateur, connecté à ton compte. Ouvre la console (touche F12, onglet Console), colle le code, puis appuie sur Entrée.

Userscript

Installe une extension de gestion de scripts (par exemple Violentmonkey ou Tampermonkey), puis ouvre le lien du fichier « .user.js » : l’extension affiche le code avant de l’installer.

YouTube

L’outil YouTube est suspendu : une vérification a montré qu’il pouvait ouvrir le formulaire de signalement au lieu du blocage. Il revient après correction et vérification.

X

vérifié sur page simulée

Bloque sur ton compte X les comptes de la liste, un par un, en envoyant depuis ta page la requête de blocage du site web de X, avec ta session. Il vérifie dans chaque réponse de X que le compte bloqué est bien celui demandé, et s'arrête sinon. La liste n'est pas écrite dans le script : au lancement, il ouvre une petite fenêtre du site Hors fil, qui demande au site la liste à jour et la lui transmet. Il avance avec une pause fixe de 3,5 secondes entre deux comptes et au plus 40 comptes par lancement ; un nouveau lancement reprend là où le précédent s'est arrêté.

Où le lancer
Sur x.com, avec ton compte ouvert : colle x.console.js dans la console du navigateur (F12, onglet Console) puis clique sur « Recevoir la liste et lancer », ou installe x.user.js dans un gestionnaire de scripts (Violentmonkey, Tampermonkey) puis clique sur « Lancer » dans l'encadré Hors fil. Une petite fenêtre du site Hors fil s'ouvre alors et se referme seule ; si ton navigateur la bloque, autorise les fenêtres pop-up pour x.com.
Méthode
requêtes de la page
Comptes
1 021 dans la liste, mis à jour en direct
Par lancement
40 comptes au plus, une pause de 3,5 secondes entre deux
Version
2.0.0, vérifiée le 30 octobre 2026 sur page simulée

Ce qu’il ne fait pas

  • N'envoie aucune donnée ailleurs que chez X et ne charge aucun code distant : il lit seulement le jeton public de l'application web de X dans le fichier de script que la page de X charge déjà. Il n'envoie lui-même aucune requête au site Hors fil : il en ouvre seulement une page dans une petite fenêtre, et c'est cette page qui demande la liste au site.
  • N'accepte la liste que de la fenêtre du site qu'il vient d'ouvrir, et la refuse en entier si un seul identifiant n'a pas la forme d'un identifiant X ; il n'exécute rien de ce qu'il reçoit.
  • Ne bloque rien, si la liste a beaucoup grossi depuis ton dernier lancement (plus de 10 % et plus de 200 comptes de plus), avant que tu confirmes : c'est une protection si le site Hors fil était compromis.
  • N'enregistre pas la liste : elle reste en mémoire le temps du lancement, et chaque lancement la redemande au site.
  • Ne garde sa progression (la liste des comptes déjà traités) que dans ton navigateur : le script console la garde dans le localStorage de la page de X, que les scripts de X peuvent lire ; le script utilisateur la garde dans le stockage de ton gestionnaire de scripts, hors de portée des pages de X.
  • Ne suit, ne signale et ne contacte aucun compte ; le lien « Suivre @BlockFarRight » affiché à la fin n'est qu'un lien.
  • Ne contourne aucune limite : il s'arrête dès que X signale une limite de débit.
  • Ne se lance pas si cette copie est trop ancienne pour la liste du site : il te demande alors de télécharger la nouvelle version sur https://onbloquelextremedroite.org/outils/.
  • N'a pas encore été essayé sur le vrai site X, seulement sur une page simulée ; si X a changé son site, il s'arrête avec un message.

Code pour la console

/*
 * Hors fil — X
 * Script console, version 2.0.0, générée le 2026-10-31.
 * https://onbloquelextremedroite.org/outils/
 *
 * CE QU'IL FAIT
 * Lancé sur https://x.com avec ton compte ouvert, il bloque sur ton compte
 * les comptes de la liste X de Hors fil, un par un. Pour chaque compte, il
 * envoie depuis ta page la requête de blocage du site web de X (POST
 * /i/api/1.1/blocks/create.json), avec ta session, et vérifie que le compte
 * bloqué que X renvoie (screen_name) est bien celui demandé ; sinon il
 * s'arrête.
 * - La liste n'est pas écrite dans ce fichier. Au lancement, pendant ton
 *   clic sur « Recevoir la liste et lancer », le script ouvre une petite
 *   fenêtre du site Hors fil
 *   (https://onbloquelextremedroite.org/outils/pont/), une nouvelle à chaque
 *   lancement. Quand elle est prête, le script lui demande la liste ; elle
 *   la demande alors au site et la transmet au script (postMessage), puis le
 *   script la ferme. Le script n'accepte que les messages de cette fenêtre,
 *   venus de https://onbloquelextremedroite.org et portant le jeton
 *   aléatoire tiré pour ce lancement (crypto.getRandomValues). Il refuse
 *   toute la liste si elle compte plus de 20 000 identifiants ou si un seul
 *   n'a pas exactement la forme d'un identifiant X (MOTIF_IDENTIFIANT) ; il
 *   n'exécute rien de ce qu'il reçoit. L'encadré montre seulement le nombre
 *   de comptes reçus, jamais la liste. Il la garde en mémoire le temps du
 *   lancement, sans l'enregistrer ; chaque lancement la redemande.
 * - Version minimale : le site indique la plus ancienne version acceptée. Si
 *   cette copie est plus ancienne, le site ne lui transmet pas la liste ou
 *   le script la refuse : télécharge alors la nouvelle version sur
 *   https://onbloquelextremedroite.org/outils/.
 * - Changement brutal : le script retient, avec sa progression, la taille et
 *   le repère de la dernière liste acceptée. Si une nouvelle liste compte
 *   plus de 10 % et plus de 200 comptes de plus, il ne bloque rien avant ta
 *   confirmation (bouton « Confirmer et continuer ») : c'est une protection
 *   si le site Hors fil était compromis. Jamais au premier lancement.
 * - Pause fixe de 3,5 s entre deux comptes (PAUSE_MS) : c'est pour rester
 *   sous les limites de débit de X.
 * - Au plus PLAFOND comptes par lancement (40 par défaut). Relance-le pour
 *   continuer.
 * - Progression gardée dans ce navigateur, dans le localStorage de la page
 *   de X (clé « horsfil.x.v1 ») ; les scripts de X peuvent la lire. Un
 *   nouveau lancement saute les comptes déjà traités. Pour tout recommencer,
 *   tape dans la console de https://x.com :
 *   localStorage.removeItem('horsfil.x.v1')
 * - Un encadré montre l'avancement, le compte en cours et un bouton
 *   « Arrêter ». À la fin, il affiche un bilan et un simple lien « Suivre
 *   @BlockFarRight » ; rien n'est suivi automatiquement.
 * - Il s'arrête avec un message clair : fenêtre du site bloquée, liste non
 *   reçue ou refusée, copie trop ancienne, pas de session, élément ou
 *   requête introuvable, limite de débit (HTTP 429 ou message de X), plafond
 *   atteint, arrêt demandé.
 * - Il ne contacte que https://x.com et https://abs.twimg.com (le serveur de
 *   fichiers de X, pour lire le jeton public de l'application web dans le
 *   script que la page charge déjà). Il n'envoie lui-même aucune requête au
 *   site Hors fil : il ouvre seulement sa page de transmission de la liste,
 *   dans une petite fenêtre. Il n'envoie aucune donnée ailleurs et ne charge
 *   aucun code distant.
 * - DRY = true : essai à blanc ; il reçoit la liste comme d'habitude, puis
 *   affiche ce qu'il ferait, sans rien envoyer à X ni enregistrer.
 *
 * COMMENT LE LANCER
 * Ouvre https://x.com avec ton compte. Ouvre la console du navigateur (F12,
 * puis l'onglet Console), colle tout le contenu de ce fichier et appuie sur
 * Entrée. Chrome peut d'abord te demander de taper « allow pasting ». Un
 * encadré « Hors fil » s'affiche : rien ne se passe tant que tu ne cliques
 * pas sur « Recevoir la liste et lancer ». Une petite fenêtre du site Hors
 * fil s'ouvre, transmet la liste et se referme ; si ton navigateur la
 * bloque, autorise les fenêtres pop-up pour x.com, puis relance.
 *
 * RÉGLAGES
 * Juste en dessous : DRY, PLAFOND, PAUSE_MS.
 *
 * ESSAIS
 * Vérifié le 2026-10-30 sur une page simulée qui imite X. Pas encore essayé
 * sur le vrai site : les repères peuvent avoir changé.
 *
 * SÉCURITÉ
 * Compare l'empreinte SHA-256 de ce fichier avec celle de la page des
 * outils. Ne colle et n'installe jamais du code qui ne vient pas de cette
 * page.
 */
(() => {
  'use strict';

  // =====================================================================
  // Réglages : tu peux les modifier avant de lancer.
  // =====================================================================

  // DRY = true : essai à blanc. Le script reçoit la liste, puis affiche dans
  // la console ce qu'il ferait, sans rien envoyer à X ni enregistrer.
  // Par défaut : false (il bloque).
  const DRY = false;

  // PLAFOND : nombre maximal de comptes traités par lancement. Relance le
  // script pour continuer : les comptes déjà traités sont sautés.
  const PLAFOND = 40;

  // PAUSE_MS : pause fixe entre deux comptes, en millisecondes, pour rester
  // sous les limites de débit de X.
  const PAUSE_MS = 3500;

  // =====================================================================
  // Données, écrites à la génération. La liste des comptes n'y est pas :
  // au lancement, une petite fenêtre du site la transmet au script (voir
  // « Réception de la liste », plus bas).
  // =====================================================================

  const VERSION = "2.0.0";
  const GENERE_LE = "2026-10-31";
  const SITE_OUTILS = "https://onbloquelextremedroite.org/outils/";
  // Site Hors fil : seule origine dont le script accepte la liste, et page
  // « pont » qu'il ouvre au lancement, dans une petite fenêtre, pour la recevoir.
  const SITE_ORIGINE = "https://onbloquelextremedroite.org";
  const SITE_PONT = "https://onbloquelextremedroite.org/outils/pont/";
  // Attente maximale de la liste, en millisecondes.
  const DELAI_PONT_MS = 90000;
  // « userscript » : progression dans le stockage du gestionnaire de scripts ;
  // « console » : dans le localStorage de la page.
  const SORTE = "console";
  const PLATEFORME = "x";
  const LIBELLE = "X";
  const ORIGINE = "https://x.com";
  // Serveur de fichiers de X, d'où la page charge son script principal.
  const ORIGINE_STATIQUE = "https://abs.twimg.com";
  const CLE = "horsfil.x.v1";
  const SUIVRE_URL = "https://x.com/intent/follow?screen_name=BlockFarRight";

  // Forme d'un identifiant X. La liste reçue est refusée en entier
  // si un seul identifiant ne correspond pas exactement à ce motif.
  const MOTIF_IDENTIFIANT = /^(?:[A-Za-z0-9_]{1,15})$/;

  // =====================================================================
  // Fonctions communes. Ne modifie rien en dessous de cette ligne.
  // =====================================================================

  const PREFIXE = '[Hors fil — ' + LIBELLE + ']';
  const SECONDES_PAUSE = String(PAUSE_MS / 1000).replace('.', ',');
  // Un lancement qui ne donne plus signe de vie depuis 30 s est considéré
  // comme terminé (onglet fermé, page rechargée).
  const BATTEMENT_PERIME_MS = 30000;
  const DRAPEAU = '__horsfil_' + PLATEFORME;
  // Une liste reçue compte de 1 à IDENTIFIANTS_MAX identifiants.
  const IDENTIFIANTS_MAX = 20000;
  const COULEURS = {
    fond: '#F7F5F0',
    encre: '#111317',
    encre2: '#4A4F57',
    accent: '#0E6E78',
    teinte: '#DCEFEF',
  };

  // Raisons d'arrêt, affichées dans l'encadré et dans la console.
  const MESSAGES = {
    termine: 'Terminé : tous les comptes de la liste ' + LIBELLE + ' ont été traités.',
    plafond: 'Plafond atteint : ' + PLAFOND + ' comptes pour ce lancement. Relance plus tard pour continuer ; les comptes déjà traités seront sautés.',
    utilisateur: 'Arrêté à ta demande. Relance quand tu veux ; les comptes déjà traités seront sautés.',
    non_connecte: 'Arrêt : aucune session ' + LIBELLE + ' active. Connecte-toi à ton compte ' + LIBELLE + ', puis relance.',
    limite: 'Arrêt : ' + LIBELLE + ' signale une limite de débit. Attends quelques heures avant de relancer ; les comptes déjà traités seront sautés.',
    introuvable: 'Arrêt : élément ou requête introuvable. ' + LIBELLE + ' a peut-être changé son site ; le script ne va pas plus loin.',
    mauvais_site: 'Ce script se lance sur ' + ORIGINE + ', pas sur cette page.',
    deja_en_cours: 'Un lancement est déjà en cours, dans cet onglet ou dans un autre.',
    erreur: 'Arrêt : erreur inattendue. Rien d\'autre n\'a été envoyé.',
    stockage: 'Arrêt : ton gestionnaire de scripts ne donne pas accès à son stockage (GM_getValue, GM_setValue) ; le script ne peut pas garder sa progression. Utilise Violentmonkey ou Tampermonkey.',
    mauvais_compte: 'Arrêt de sécurité : la réponse de ' + LIBELLE + ' ne correspond pas au compte demandé. Le script s\'arrête pour ne pas bloquer un autre compte.',
    clic_refuse: 'Arrêt de sécurité : l\'élément trouvé porte un libellé interdit (signaler, envoyer, supprimer, ne plus suivre…). Le script ne clique jamais sur un tel élément : il n\'a pas cliqué. ' + LIBELLE + ' a peut-être changé son site.',
    formulaire: 'Arrêt de sécurité : une boîte de signalement est ouverte. Ferme-la sans rien envoyer ; le script ne remplit et n\'envoie jamais de formulaire.',
    non_verifie: 'Arrêt : le blocage de deux comptes de suite n\'a pas pu être vérifié sur la page. Vérifie ces comptes à la main ; ' + LIBELLE + ' a peut-être changé son site.',
    sans_reponse: 'Arrêt : le script attendait ton clic et ne l\'a pas vu venir. Relance quand tu veux ; les comptes déjà traités seront sautés.',
    // Réception de la liste (textes du protocole du site).
    pont_bloque: 'Ton navigateur a bloqué la fenêtre du site Hors fil. Autorise les fenêtres pop-up pour ' + location.host + ', puis relance.',
    pont_delai: 'La liste n\'est pas arrivée. Vérifie que la fenêtre du site s\'est ouverte, puis relance.',
    trop_ancienne: 'Cette copie de l\'outil est trop ancienne. Télécharge la nouvelle version sur ' + SITE_OUTILS + '.',
    pont_erreur: 'Arrêt : le site Hors fil n\'a pas transmis la liste. Réessaie plus tard.',
    liste_invalide: 'Arrêt de sécurité : la liste reçue n\'a pas la forme attendue. Le script ne s\'en sert pas : rien n\'a été bloqué.',
    liste_absente: 'Arrêt : la liste reçue pour ce lancement n\'est plus là (effacée, ou reçue il y a plus de 24 heures). Relance pour la recevoir de nouveau ; les comptes déjà traités seront sautés.',
  };

  let arretDemande = false;

  function journal(...morceaux) {
    console.info(PREFIXE, ...morceaux);
  }

  function avertir(...morceaux) {
    console.warn(PREFIXE, ...morceaux);
  }

  function dormir(ms) {
    return new Promise((suite) => setTimeout(suite, ms));
  }

  function cle(identifiant) {
    return String(identifiant).toLowerCase();
  }

  // Un compte est soit un identifiant, soit [identifiant, chemin du profil].
  function identifiant(compte) {
    return Array.isArray(compte) ? compte[0] : compte;
  }

  // Espaces insécables de la typographie française.
  function typo(texte) {
    return String(texte).replace(/ ([:;!?»])/g, ' $1').replace(/« /g, '« ');
  }

  function lireCookie(nom) {
    for (const morceau of document.cookie.split(';')) {
      const i = morceau.indexOf('=');
      if (i < 0) continue;
      if (morceau.slice(0, i).trim() !== nom) continue;
      const valeur = morceau.slice(i + 1).trim();
      try {
        return decodeURIComponent(valeur);
      } catch (e) {
        return valeur;
      }
    }
    return '';
  }

  // Raison d'arrêt. affiche : texte à montrer à la place du message
  // habituel (par exemple, le message transmis par le site).
  class Arret extends Error {
    constructor(code, detail, affiche) {
      super(detail || code);
      this.code = code;
      this.detail = detail || '';
      this.affiche = affiche || '';
    }
  }

  // ---------------------------------------------------------------------
  // Progression, gardée dans ce navigateur sous la clé CLE :
  // - script utilisateur : dans le stockage du gestionnaire de scripts
  //   (GM_getValue, GM_setValue), que les pages de la plateforme ne peuvent
  //   pas lire ;
  // - script console : dans le localStorage de la page de la plateforme
  //   (seul stockage possible pour du code collé dans la console ; les
  //   scripts de la plateforme peuvent le lire).
  // faits : { identifiant en minuscules : 'b' (bloqué), 'd' (l'était déjà),
  //           'i' (non trouvé), 'n' (blocage non vérifié) }
  // lancement : le lancement en cours (onglet, battement, compteurs).
  // derniere : { liste, n }, repère et taille de la dernière liste acceptée
  // (voir « Changement brutal de la liste »).
  // En mode DRY, rien n'est écrit. La liste elle-même n'est jamais écrite
  // ici (voir « Réception de la liste »).
  // ---------------------------------------------------------------------

  function stockageGestionnaire() {
    return typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
  }

  // Faux seulement pour un script utilisateur dont le gestionnaire ne donne
  // pas accès à son stockage : il ne se lance pas.
  function stockageDisponible() {
    return SORTE !== 'userscript' || stockageGestionnaire();
  }

  function lireBrut() {
    if (SORTE === 'userscript') {
      if (!stockageGestionnaire()) return null;
      const v = GM_getValue(CLE, null);
      return typeof v === 'string' ? v : null;
    }
    return localStorage.getItem(CLE);
  }

  function ecrireBrut(texte) {
    if (SORTE === 'userscript') {
      if (stockageGestionnaire()) GM_setValue(CLE, texte);
      return;
    }
    localStorage.setItem(CLE, texte);
  }

  function effacerBrut() {
    if (SORTE === 'userscript') {
      if (typeof GM_deleteValue === 'function') GM_deleteValue(CLE);
      else if (stockageGestionnaire()) GM_setValue(CLE, null);
      return;
    }
    localStorage.removeItem(CLE);
  }

  function etatVide() {
    return { v: 1, faits: {}, lancement: null };
  }

  function lireEtat() {
    let brut = null;
    try {
      brut = lireBrut();
    } catch (e) {
      brut = null;
    }
    let etat = null;
    try {
      etat = brut ? JSON.parse(brut) : null;
    } catch (e) {
      etat = null;
    }
    if (!etat || etat.v !== 1 || typeof etat.faits !== 'object' || etat.faits === null) {
      return etatVide();
    }
    if (typeof etat.lancement !== 'object') etat.lancement = null;
    return etat;
  }

  function ecrireEtat(etat) {
    if (DRY) return;
    try {
      ecrireBrut(JSON.stringify(etat));
    } catch (e) {
      avertir('Impossible d\'enregistrer la progression :', e);
    }
  }

  // Commande du gestionnaire de scripts : tout recommencer.
  function effacerProgression() {
    const etat = lireEtat();
    if (etat.lancement && etat.lancement.actif && Date.now() - (etat.lancement.battement || 0) < BATTEMENT_PERIME_MS) {
      avertir('Un lancement est en cours : arrête-le avant d\'effacer la progression.');
      return;
    }
    if (typeof confirm === 'function' && !confirm(typo('Hors fil — ' + LIBELLE + ' : effacer la progression ? Les comptes déjà traités seront de nouveau proposés.'))) return;
    try {
      effacerBrut();
      // Outils par l'interface : la liste gardée part aussi.
      if (typeof effacerListeGardee === 'function') effacerListeGardee();
      journal('Progression effacée.');
    } catch (e) {
      avertir('Impossible d\'effacer la progression :', e);
    }
  }

  // Comptes de la liste reçue qui restent à traiter.
  function compterRestants(etat, comptes) {
    return comptes.filter((c) => !etat.faits[cle(identifiant(c))]).length;
  }

  // Identifiant aléatoire de cet onglet, sans aucune donnée : il sert
  // seulement à savoir quel onglet mène le lancement en cours (pas deux
  // lancements à la fois, reprise dans le bon onglet). Il est gardé dans le
  // sessionStorage de la page (clé « horsfil.onglet »), qui survit aux
  // changements de page dans le même onglet, mais pas d'un onglet à l'autre.
  function lireIdOnglet() {
    const nom = 'horsfil.onglet';
    try {
      let id = sessionStorage.getItem(nom);
      if (!id) {
        id = Math.random().toString(36).slice(2) + Date.now().toString(36);
        sessionStorage.setItem(nom, id);
      }
      return id;
    } catch (e) {
      return Math.random().toString(36).slice(2) + Date.now().toString(36);
    }
  }
  const ONGLET = lireIdOnglet();

  function lancementActifAilleurs(etat) {
    const l = etat.lancement;
    return !!(l && l.actif && l.onglet !== ONGLET && Date.now() - (l.battement || 0) < BATTEMENT_PERIME_MS);
  }

  let dernierBattement = 0;
  function battre() {
    if (DRY) return;
    const maintenant = Date.now();
    if (maintenant - dernierBattement < 2000) return;
    dernierBattement = maintenant;
    const etat = lireEtat();
    if (etat.lancement && etat.lancement.actif && etat.lancement.onglet === ONGLET) {
      etat.lancement.battement = maintenant;
      ecrireEtat(etat);
    }
  }

  // Pause fixe, interrompue dès que tu cliques sur « Arrêter ».
  async function pause(ms) {
    const fin = Date.now() + ms;
    while (Date.now() < fin) {
      if (arretDemande) return;
      await dormir(Math.min(100, fin - Date.now()));
      battre();
    }
  }

  // ---------------------------------------------------------------------
  // Réception de la liste. La liste n'est pas écrite dans ce script : au
  // lancement, pendant ton clic, le script ouvre une petite fenêtre du site
  // Hors fil (la page « pont », SITE_PONT), une nouvelle à chaque lancement.
  // Quand elle est prête (message horsfil-pret), le script lui demande la
  // liste (horsfil-demande) : c'est seulement alors qu'elle la demande au
  // site. Elle la transmet au script par postMessage ; le script répond
  // « reçu » (horsfil-recu) et ferme la fenêtre.
  // Le script n'envoie lui-même aucune requête au site : l'adresse de la
  // fenêtre porte seulement, après « # » (partie qui ne part pas vers le
  // serveur), la plateforme, l'origine de cette page, un jeton aléatoire et
  // la version du script.
  //
  // Le script ignore tout message qui ne vient pas exactement de
  // SITE_ORIGINE, de la fenêtre qu'il vient d'ouvrir, ou qui ne porte pas
  // le jeton tiré pour cette ouverture. Il refuse toute la liste (rien n'est
  // bloqué) si elle ne vise pas cette plateforme, si elle compte 0 ou plus
  // de IDENTIFIANTS_MAX identifiants, ou si un seul identifiant n'a pas
  // exactement la forme MOTIF_IDENTIFIANT. La liste n'est qu'une liste
  // d'identifiants : rien de ce qu'elle contient n'est exécuté.
  // ---------------------------------------------------------------------

  const MOTIF_VERSION = /^(\d{1,9})\.(\d{1,9})\.(\d{1,9})$/;
  // Repère de la liste transmise par le site : 12 caractères hexadécimaux.
  const MOTIF_REPERE_LISTE = /^[0-9a-fA-F]{12}$/;

  // Jeton de 32 caractères hexadécimaux minuscules, tiré au hasard
  // (crypto.getRandomValues) à chaque ouverture de la fenêtre.
  function tirerJeton() {
    const octets = new Uint8Array(16);
    crypto.getRandomValues(octets);
    return [...octets].map((o) => o.toString(16).padStart(2, '0')).join('');
  }

  // Un objet ordinaire ({…}) : ni tableau, ni texte, ni autre sorte d'objet.
  function objetOrdinaire(v) {
    if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
    if (Object.prototype.toString.call(v) !== '[object Object]') return false;
    const proto = Object.getPrototypeOf(v);
    return proto === null || Object.getPrototypeOf(proto) === null;
  }

  // [X, Y, Z] pour « X.Y.Z », sinon null.
  function nombresVersion(v) {
    const m = MOTIF_VERSION.exec(typeof v === 'string' ? v : '');
    return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
  }

  // Compare deux versions nombre par nombre (1.10.0 vient après 1.9.0) :
  // négatif si a < b, 0 si égales, positif si a > b.
  function comparerVersions(a, b) {
    for (let i = 0; i < 3; i++) {
      if (a[i] !== b[i]) return a[i] - b[i];
    }
    return 0;
  }

  // Ouvre la page pont. À appeler pendant ton clic et avant toute attente :
  // sans clic, le navigateur bloque la fenêtre.
  function ouvrirPont() {
    const pont = { jeton: '', fenetre: null, ouverte: Date.now(), reponse: null, ecouteur: null, erreur: '' };
    try {
      pont.jeton = tirerJeton();
    } catch (e) {
      pont.erreur = 'tirage du jeton impossible (crypto.getRandomValues) : ' + ((e && e.message) || e);
      return pont;
    }
    // Contrôles de chaque message : origine, source, objet ordinaire, jeton.
    // « Prête » reçoit la demande de la liste ; seule la première réponse
    // (liste ou erreur) est gardée.
    pont.ecouteur = (ev) => {
      if (pont.reponse) return;
      if (ev.origin !== SITE_ORIGINE) return;
      if (!pont.fenetre || ev.source !== pont.fenetre) return;
      const d = ev.data;
      if (!objetOrdinaire(d) || d.n !== pont.jeton) return;
      if (d.type === 'horsfil-pret') {
        // La page pont ne demande la liste au site qu'après cette réponse.
        try {
          ev.source.postMessage({ type: 'horsfil-demande', n: pont.jeton }, SITE_ORIGINE);
          journal('Fenêtre du site prête : demande de la liste envoyée.');
        } catch (e) {
          avertir('Demande de la liste non transmise à la fenêtre du site :', e);
        }
        return;
      }
      if (d.type !== 'horsfil-liste' && d.type !== 'horsfil-erreur') return;
      pont.reponse = { donnees: d, source: ev.source };
    };
    window.addEventListener('message', pont.ecouteur);
    const adresse = SITE_PONT + '#p=' + PLATEFORME + '&o=' + encodeURIComponent(location.origin)
      + '&n=' + pont.jeton + '&v=' + VERSION;
    try {
      // Un nom de fenêtre par lancement : jamais une ancienne fenêtre reprise.
      pont.fenetre = window.open(adresse, 'horsfil-pont-' + pont.jeton, 'popup=yes,width=480,height=640');
    } catch (e) {
      pont.fenetre = null;
    }
    if (!pont.fenetre) window.removeEventListener('message', pont.ecouteur);
    return pont;
  }

  // Ferme la fenêtre du site, au mieux : juste après l'accusé de réception,
  // et quand le script renonce à la liste (refus, délai dépassé, arrêt).
  function fermerPont(pont) {
    try {
      if (pont.fenetre && !pont.fenetre.closed) pont.fenetre.close();
    } catch (e) {
      // fenêtre déjà fermée
    }
  }

  // Texte transmis par le site pour être affiché : une ligne, sans mise en
  // forme (il n'est jamais interprété comme du code ou du HTML).
  function texteDuSite(t) {
    return typeof t === 'string' ? t.replace(/\s+/g, ' ').trim().slice(0, 300) : '';
  }

  // Contrôles de la liste, avant tout blocage. Un seul échec arrête le
  // lancement, sans rien bloquer. Renvoie { liste, comptes } : comptes sans
  // doublons (sans tenir compte de la casse), dans l'ordre reçu.
  function verifierListe(d) {
    if (d.plateforme !== PLATEFORME) {
      throw new Arret('liste_invalide', 'liste prévue pour une autre plateforme (« '
        + String(d.plateforme).slice(0, 20) + ' » au lieu de « ' + PLATEFORME + ' »)');
    }
    // Version d'abord : une copie trop ancienne ne reconnaît peut-être plus
    // les identifiants de la liste.
    const minimum = nombresVersion(d.outil_min);
    if (!minimum) throw new Arret('liste_invalide', 'version minimale illisible');
    if (comparerVersions(nombresVersion(VERSION), minimum) < 0) {
      throw new Arret('trop_ancienne', 'version ' + VERSION + ', version minimale demandée par le site : ' + d.outil_min);
    }
    if (typeof d.liste !== 'string' || !MOTIF_REPERE_LISTE.test(d.liste)) {
      throw new Arret('liste_invalide', 'repère de la liste illisible');
    }
    const recus = d.comptes;
    if (!Array.isArray(recus) || recus.length < 1 || recus.length > IDENTIFIANTS_MAX) {
      throw new Arret('liste_invalide', Array.isArray(recus)
        ? recus.length + ' identifiants reçus, alors qu\'il en faut de 1 à ' + IDENTIFIANTS_MAX
        : 'pas de tableau d\'identifiants');
    }
    const comptes = [];
    const vus = new Set();
    for (let i = 0; i < recus.length; i++) {
      const h = recus[i];
      if (typeof h !== 'string' || !MOTIF_IDENTIFIANT.test(h)) {
        throw new Arret('liste_invalide', 'l\'identifiant n° ' + (i + 1) + ' sur ' + recus.length
          + ' n\'a pas la forme d\'un identifiant ' + LIBELLE + ' ; toute la liste est refusée');
      }
      if (vus.has(cle(h))) continue;
      vus.add(cle(h));
      comptes.push(h);
    }
    return { liste: d.liste, comptes };
  }

  // Attend la réponse de la page pont (DELAI_PONT_MS au plus, depuis
  // l'ouverture), vérifie la liste, envoie l'accusé de réception et ferme
  // la fenêtre (elle se ferme aussi d'elle-même). Renvoie { liste, comptes },
  // ou lève un Arret (rien n'est bloqué).
  async function recevoirListe(pont, panneau) {
    if (pont.erreur) throw new Arret('erreur', pont.erreur);
    if (!pont.fenetre) throw new Arret('pont_bloque', 'window.open n\'a pas ouvert la fenêtre');
    panneau.statut('Une petite fenêtre du site Hors fil s\'est ouverte : elle demande la liste au site et la transmet au script…');
    journal('Fenêtre du site Hors fil ouverte (' + SITE_PONT + ') : attente de la liste.');
    const fin = pont.ouverte + DELAI_PONT_MS;
    try {
      while (!pont.reponse) {
        if (arretDemande) throw new Arret('utilisateur');
        if (Date.now() >= fin) {
          throw new Arret('pont_delai', 'aucune réponse de la fenêtre du site en ' + Math.round(DELAI_PONT_MS / 1000) + ' s');
        }
        await dormir(100);
        battre();
      }
      const d = pont.reponse.donnees;
      if (d.type === 'horsfil-erreur') {
        const erreur = typeof d.erreur === 'string' ? d.erreur.slice(0, 20) : '';
        throw new Arret(erreur === 'version' ? 'trop_ancienne' : 'pont_erreur',
          'erreur « ' + (erreur || '?') + ' » signalée par le site', texteDuSite(d.message));
      }
      const recue = verifierListe(d);
      // Accusé de réception ; la fenêtre est fermée juste après (finally).
      try {
        pont.reponse.source.postMessage({ type: 'horsfil-recu', n: pont.jeton }, SITE_ORIGINE);
      } catch (e) {
        avertir('Accusé de réception non transmis à la fenêtre du site :', e);
      }
      journal('Liste reçue du site : ' + recue.comptes.length + ' comptes (liste ' + recue.liste + ').');
      return recue;
    } finally {
      window.removeEventListener('message', pont.ecouteur);
      fermerPont(pont);
    }
  }

  // ---------------------------------------------------------------------
  // Changement brutal de la liste. Le script retient, dans sa progression
  // (champ derniere : { liste, n }), le repère et la taille de la dernière
  // liste acceptée. Si une nouvelle liste compte plus de max(n × 1,10 ;
  // n + 200) comptes, il ne bloque rien avant ta confirmation : si le site
  // Hors fil était compromis, une liste gonflée ne passerait pas sans que tu
  // le voies. Jamais au premier lancement (aucune liste retenue).
  // ---------------------------------------------------------------------

  // Taille de la dernière liste acceptée si la nouvelle la dépasse
  // brutalement, sinon null.
  function changementBrutal(etat, nombre) {
    const d = etat.derniere;
    if (!d || typeof d !== 'object' || !Number.isInteger(d.n) || d.n < 1) return null;
    // nombre > max(n × 1,1 ; n + 200), compté en entiers.
    return (nombre * 10 > d.n * 11 && nombre > d.n + 200) ? d.n : null;
  }

  // Avant tout blocage : ta confirmation si la liste a beaucoup changé,
  // puis cette liste devient la dernière acceptée (rien n'est écrit en DRY).
  async function accepterListe(recue, panneau) {
    const avant = changementBrutal(lireEtat(), recue.comptes.length);
    if (avant !== null) {
      const texte = 'La liste a beaucoup changé depuis ton dernier lancement : ' + recue.comptes.length
        + ' comptes au lieu de ' + avant + '. Vérifie sur la page des outils que tout est normal, puis confirme.';
      avertir(texte);
      if (!(await panneau.confirmer(texte))) throw new Arret('utilisateur');
      journal('Changement de la liste confirmé : ' + recue.comptes.length + ' comptes au lieu de ' + avant + '.');
    }
    const etat = lireEtat();
    etat.derniere = { liste: recue.liste, n: recue.comptes.length };
    ecrireEtat(etat);
  }

  // ---------------------------------------------------------------------
  // Encadré affiché sur la page (construit avec des nœuds DOM, sans HTML).
  // ---------------------------------------------------------------------

  function noeud(balise, styles, texte) {
    const e = document.createElement(balise);
    if (styles) Object.assign(e.style, styles);
    if (texte !== undefined && texte !== null) e.textContent = typo(texte);
    return e;
  }

  function bouton(texte, action, principal) {
    const b = noeud('button', {
      font: 'inherit',
      fontWeight: '700',
      minHeight: '44px',
      padding: '8px 14px',
      margin: '8px 8px 0 0',
      borderRadius: '6px',
      border: '2px solid ' + COULEURS.accent,
      cursor: 'pointer',
      background: principal ? COULEURS.accent : COULEURS.fond,
      color: principal ? COULEURS.fond : COULEURS.accent,
    }, texte);
    b.type = 'button';
    b.dataset.action = action;
    return b;
  }

  function lienOutils() {
    const lien = noeud('a', { display: 'inline-block', marginTop: '6px', color: COULEURS.accent, fontWeight: '700', textDecoration: 'underline' }, 'Page des outils Hors fil');
    lien.href = SITE_OUTILS;
    lien.target = '_blank';
    lien.rel = 'noopener noreferrer';
    return lien;
  }

  const STYLE_BOITE = {
    position: 'fixed',
    bottom: '16px',
    zIndex: '2147483647',
    boxSizing: 'border-box',
    maxWidth: 'calc(100vw - 32px)',
    padding: '14px 16px',
    background: COULEURS.fond,
    color: COULEURS.encre,
    border: '2px solid ' + COULEURS.accent,
    borderRadius: '8px',
    boxShadow: '0 6px 24px rgba(0, 0, 0, 0.25)',
    font: '15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
    textAlign: 'left',
  };

  function demanderArret(panneau) {
    if (arretDemande) return;
    arretDemande = true;
    journal('Arrêt demandé.');
    const etat = lireEtat();
    if (etat.lancement && etat.lancement.onglet === ONGLET) {
      etat.lancement.actif = false;
      ecrireEtat(etat);
    }
    // Outils par l'interface : la liste gardée est effacée dès l'arrêt.
    if (typeof effacerListeGardee === 'function') effacerListeGardee();
    panneau.statut('Arrêt demandé…');
  }

  function creerPanneau() {
    for (const id of ['horsfil-panneau', 'horsfil-lanceur']) {
      const ancien = document.getElementById(id);
      if (ancien) ancien.remove();
    }
    const racine = noeud('section', Object.assign({}, STYLE_BOITE, {
      right: '16px',
      width: '340px',
      maxHeight: '80vh',
      overflow: 'auto',
    }));
    racine.id = 'horsfil-panneau';
    racine.setAttribute('aria-label', 'Hors fil — ' + LIBELLE);
    racine.dataset.etat = 'en_cours';
    const elTitre = noeud('p', { margin: '0 0 6px', fontWeight: '700', fontSize: '16px' },
      'Hors fil — ' + LIBELLE + (DRY ? ' · essai à blanc (DRY)' : ''));
    // Nombre de comptes reçus du site : la liste elle-même n'est jamais affichée.
    const elListe = noeud('p', { margin: '0 0 4px', display: 'none' }, '');
    const elProgression = noeud('p', { margin: '0 0 4px' }, '');
    const elCourant = noeud('p', { margin: '0 0 4px', fontFamily: 'ui-monospace, Menlo, Consolas, monospace' }, '');
    const elStatut = noeud('p', { margin: '0 0 4px', color: COULEURS.encre2 }, 'Préparation…');
    elStatut.setAttribute('role', 'status');
    elStatut.setAttribute('aria-live', 'polite');
    const elDetail = noeud('p', { margin: '4px 0 0', color: COULEURS.encre2, fontSize: '13px', display: 'none' }, '');
    // Consigne quand le script attend un clic de ta part (YouTube).
    const elAttente = noeud('p', { margin: '6px 0 0', padding: '8px 10px', fontWeight: '700', border: '2px solid ' + COULEURS.accent, borderRadius: '6px', display: 'none' }, '');
    elAttente.setAttribute('role', 'alert');
    // Demande de confirmation quand la liste a beaucoup changé.
    const elConfirmation = noeud('div', { margin: '6px 0 0', padding: '8px 10px', border: '2px solid ' + COULEURS.accent, borderRadius: '6px', display: 'none' });
    elConfirmation.setAttribute('role', 'alert');
    const elConfirmer = bouton('Confirmer et continuer', 'confirmer', true);
    let confirme = false;
    elConfirmer.addEventListener('click', () => {
      confirme = true;
    });
    const elBilan = noeud('div', { display: 'none', marginTop: '8px', padding: '8px 10px', background: COULEURS.teinte, borderRadius: '6px' });
    const elArreter = bouton('Arrêter', 'arreter', true);
    const elFermer = bouton('Fermer', 'fermer', false);
    elFermer.style.display = 'none';
    racine.append(elTitre, elListe, elProgression, elCourant, elStatut, elAttente, elConfirmation, elDetail, elBilan, elArreter, elFermer);
    (document.body || document.documentElement).appendChild(racine);

    const panneau = {
      racine,
      liste(nombre) {
        racine.dataset.comptes = String(nombre);
        elListe.textContent = typo('Liste reçue du site : ' + nombre + (nombre > 1 ? ' comptes.' : ' compte.'));
        elListe.style.display = 'block';
      },
      progression(n, traites, total) {
        elProgression.textContent = typo((DRY ? 'Essai à blanc : ' : 'Ce lancement : ') + n + ' sur ' + PLAFOND
          + ' · Liste : ' + traites + ' sur ' + total + ' traités');
      },
      courant(h) {
        elCourant.textContent = h ? 'Compte en cours : @' + h : '';
      },
      statut(texte) {
        elStatut.textContent = typo(texte);
      },
      attente(texte) {
        if (texte) {
          racine.dataset.attente = 'manuel';
          elAttente.textContent = typo(texte);
          elAttente.style.display = 'block';
        } else {
          delete racine.dataset.attente;
          elAttente.textContent = '';
          elAttente.style.display = 'none';
        }
      },
      // Montre texte et attend ta réponse : vrai si tu confirmes, faux si tu
      // arrêtes. Rien n'est bloqué pendant l'attente.
      async confirmer(texte) {
        confirme = false;
        racine.dataset.attente = 'confirmation';
        const action = noeud('div', { marginTop: '2px' });
        action.appendChild(elConfirmer);
        elConfirmation.replaceChildren(noeud('p', { margin: '0', fontWeight: '700' }, texte), lienOutils(), action);
        elConfirmation.style.display = 'block';
        elStatut.textContent = typo('En attente de ta confirmation : rien n\'est bloqué pour l\'instant.');
        try {
          for (;;) {
            if (arretDemande) return false;
            if (confirme) return true;
            await dormir(100);
            battre();
          }
        } finally {
          delete racine.dataset.attente;
          elConfirmation.replaceChildren();
          elConfirmation.style.display = 'none';
        }
      },
      // affiche : texte à montrer à la place du message habituel (message
      // transmis par le site).
      fin(code, bilan, detail, restants, affiche) {
        racine.dataset.etat = 'fini';
        racine.dataset.raison = code;
        delete racine.dataset.attente;
        elAttente.style.display = 'none';
        elConfirmation.style.display = 'none';
        elCourant.textContent = '';
        const message = affiche || MESSAGES[code] || MESSAGES.erreur;
        elStatut.textContent = typo(message);
        elStatut.style.color = COULEURS.encre;
        elStatut.style.fontWeight = '700';
        if (detail) {
          elDetail.textContent = typo('Détail : ' + detail);
          elDetail.style.display = 'block';
        }
        if (bilan) {
          elBilan.replaceChildren();
          const lignes = DRY
            ? ['Essai à blanc : rien n\'a été envoyé.', 'Comptes qui auraient été traités : ' + bilan.bloques]
            : ['Bloqués pendant ce lancement : ' + bilan.bloques,
              'Bloqués avant ce lancement : ' + bilan.deja,
              'Non trouvés, sautés : ' + bilan.introuvables];
          if (!DRY && typeof bilan.nonVerifies === 'number') {
            lignes.push('Blocage non vérifié (à contrôler à la main) : ' + bilan.nonVerifies);
          }
          if (typeof restants === 'number') lignes.push('Restants dans la liste : ' + restants);
          for (const ligne of lignes) elBilan.appendChild(noeud('p', { margin: '0 0 2px' }, ligne));
          const lien = noeud('a', {
            display: 'inline-block',
            marginTop: '6px',
            color: COULEURS.accent,
            fontWeight: '700',
            textDecoration: 'underline',
          }, 'Suivre @BlockFarRight');
          lien.href = SUIVRE_URL;
          lien.target = '_blank';
          lien.rel = 'noopener noreferrer';
          elBilan.appendChild(lien);
          elBilan.style.display = 'block';
        }
        elArreter.style.display = 'none';
        elFermer.style.display = 'inline-block';
        const ecrire = (code === 'termine' || code === 'plafond' || code === 'utilisateur') ? journal : avertir;
        ecrire(message + (detail ? ' (' + detail + ')' : ''));
        if (code === 'trop_ancienne') {
          elBilan.replaceChildren(lienOutils());
          elBilan.style.display = 'block';
        }
        if (bilan) {
          journal('Bilan : ' + bilan.bloques + ' bloqués, ' + bilan.deja + ' bloqués avant, '
            + bilan.introuvables + ' non trouvés'
            + (typeof bilan.nonVerifies === 'number' ? ', ' + bilan.nonVerifies + ' non vérifiés' : '')
            + (typeof restants === 'number' ? ', ' + restants + ' restants.' : '.'));
        }
      },
    };
    elArreter.addEventListener('click', () => demanderArret(panneau));
    elFermer.addEventListener('click', () => racine.remove());
    return panneau;
  }

  // =====================================================================
  // X : la page envoie à son propre site la requête de blocage du site
  // web de X, avec ta session (cookies du navigateur).
  // =====================================================================

  // X indique précisément quand un compte n'existe pas (code 50) ou est
  // suspendu (code 63) : pas besoin de règle « plusieurs de suite ».
  const INTROUVABLES_DE_SUITE_MAX = 0;
  const CHEMIN_BLOCAGE = '/i/api/1.1/blocks/create.json';
  // Script principal du site web de X, tel que la page le charge.
  const MOTIF_SCRIPT_PRINCIPAL = /^\/responsive-web\/client-web(?:-legacy)?\/main\.[0-9a-z]+\.js$/;
  // Jeton public de l'application web : une chaîne entre guillemets qui
  // commence par vingt « A ». Il n'est écrit nulle part dans ce fichier :
  // il est lu à chaque lancement dans le script que la page charge déjà.
  const MOTIF_JETON = /["'](AAAAAAAAAAAAAAAAAAAA[A-Za-z0-9%]{40,160})["']/g;

  function trouverScriptPrincipal() {
    const adresses = [];
    for (const s of document.querySelectorAll('script[src]')) adresses.push(s.getAttribute('src'));
    for (const l of document.querySelectorAll('link[rel="preload"][href], link[rel="modulepreload"][href]')) {
      adresses.push(l.getAttribute('href'));
    }
    for (const brute of adresses) {
      let u;
      try {
        u = new URL(brute, location.href);
      } catch (e) {
        continue;
      }
      if (u.origin !== ORIGINE && u.origin !== ORIGINE_STATIQUE) continue;
      if (MOTIF_SCRIPT_PRINCIPAL.test(u.pathname)) return u;
    }
    return null;
  }

  async function preparer() {
    if (!lireCookie('ct0')) throw new Arret('non_connecte', 'jeton anti-CSRF (cookie ct0) absent');
    const u = trouverScriptPrincipal();
    if (!u) throw new Arret('introuvable', 'script principal de la page (main.*.js) introuvable');
    if (DRY) {
      journal('[DRY] Lirait le jeton public de l\'application web dans ' + u.href + ' (rien n\'est envoyé).');
      return { jeton: '' };
    }
    let texte = '';
    try {
      const r = await fetch(u.href, { credentials: u.origin === location.origin ? 'same-origin' : 'omit' });
      if (!r.ok) throw new Arret('introuvable', 'script principal illisible (HTTP ' + r.status + ')');
      texte = await r.text();
    } catch (e) {
      if (e instanceof Arret) throw e;
      throw new Arret('introuvable', 'script principal illisible (' + e.message + ')');
    }
    const jetons = [];
    for (const m of texte.matchAll(MOTIF_JETON)) {
      if (jetons.indexOf(m[1]) < 0) jetons.push(m[1]);
    }
    if (jetons.length === 0) {
      throw new Arret('introuvable', 'jeton public de l\'application web introuvable dans ' + u.href);
    }
    if (jetons.length > 1) avertir(jetons.length + ' jetons possibles trouvés ; le premier est utilisé.');
    journal('Jeton public de l\'application web lu dans ' + u.href + '.');
    return { jeton: jetons[0] };
  }

  async function codesErreur(reponse) {
    try {
      const j = await reponse.clone().json();
      if (j && Array.isArray(j.errors)) {
        return j.errors.map((e) => Number(e && e.code)).filter((c) => Number.isFinite(c));
      }
    } catch (e) {
      // réponse sans JSON
    }
    return [];
  }

  async function bloquer(contexte, h) {
    const ct0 = lireCookie('ct0');
    if (!ct0) throw new Arret('non_connecte', 'jeton anti-CSRF (cookie ct0) absent');
    let r;
    try {
      r = await fetch(ORIGINE + CHEMIN_BLOCAGE, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'authorization': 'Bearer ' + contexte.jeton,
          'x-csrf-token': ct0,
          'x-twitter-auth-type': 'OAuth2Session',
          'x-twitter-active-user': 'yes',
          'content-type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({ screen_name: h }).toString(),
      });
    } catch (e) {
      throw new Arret('erreur', 'requête de blocage impossible (' + e.message + ')');
    }
    const codes = await codesErreur(r);
    const resume = 'HTTP ' + r.status + (codes.length ? ', code ' + codes.join(', ') : '');
    if (r.status === 429 || codes.indexOf(88) >= 0) throw new Arret('limite', resume);
    if (r.ok) {
      // X renvoie le compte bloqué : il doit être exactement celui demandé
      // (les identifiants X ne tiennent pas compte de la casse).
      let j = null;
      try {
        j = await r.clone().json();
      } catch (e) {
        j = null;
      }
      const nom = j && typeof j.screen_name === 'string' ? j.screen_name : '';
      if (nom.toLowerCase() !== String(h).toLowerCase()) {
        throw new Arret('mauvais_compte', 'X a répondu pour ' + (nom ? '@' + nom : 'un compte sans nom')
          + ' au lieu de @' + h + ' (' + resume + '). Ouvre les comptes bloqués dans les réglages de X et débloque ce compte s\'il n\'est pas dans la liste.');
      }
      journal('@' + h + ' : bloqué.');
      return 'b';
    }
    if (codes.indexOf(50) >= 0 || codes.indexOf(63) >= 0) {
      journal('@' + h + ' : compte introuvable ou suspendu (' + resume + '), sauté.');
      return 'i';
    }
    if (r.status === 401 || r.status === 403) throw new Arret('non_connecte', resume);
    if (r.status === 404) throw new Arret('introuvable', 'requête de blocage introuvable (' + resume + ')');
    throw new Arret('erreur', 'réponse inattendue de ' + LIBELLE + ' (' + resume + ')');
  }

  // =====================================================================
  // Déroulement : un compte à la fois, par les requêtes de la page. La
  // liste, reçue du site au lancement, reste en mémoire le temps du
  // lancement : elle n'est enregistrée nulle part.
  // =====================================================================

  function prochainCompte(comptes, etat, vus) {
    for (const h of comptes) {
      if (!etat.faits[cle(h)] && !vus.has(cle(h))) return h;
    }
    return null;
  }

  // Lancé par ton clic (« Lancer » ou « Recevoir la liste et lancer ») : la
  // fenêtre du site est ouverte avant la première attente.
  async function executer() {
    if (window[DRAPEAU]) {
      avertir(MESSAGES.deja_en_cours);
      return;
    }
    arretDemande = false;
    const panneau = creerPanneau();
    if (location.origin !== ORIGINE) {
      panneau.fin('mauvais_site', null, 'page actuelle : ' + location.origin);
      return;
    }
    if (!stockageDisponible()) {
      panneau.fin('stockage', null, '');
      return;
    }
    let etat = lireEtat();
    if (lancementActifAilleurs(etat)) {
      panneau.fin('deja_en_cours', null, '');
      return;
    }
    // Pendant ton clic, avant toute attente : sans clic, le navigateur
    // bloque la fenêtre du site.
    const pont = ouvrirPont();
    window[DRAPEAU] = true;
    if (!DRY) {
      etat.lancement = { actif: true, onglet: ONGLET, battement: Date.now(), debut: Date.now() };
      ecrireEtat(etat);
    }

    let comptes = null; // liste reçue du site, en mémoire seulement
    const bilan = { bloques: 0, deja: 0, introuvables: 0 };
    const vus = new Set(); // comptes simulés en mode DRY
    const serie = []; // comptes introuvables de suite
    let n = 0;
    let code = 'termine';
    let detail = '';
    let affiche = '';
    const majProgression = () => {
      if (comptes) panneau.progression(n, comptes.length - compterRestants(lireEtat(), comptes), comptes.length);
    };

    try {
      const recue = await recevoirListe(pont, panneau);
      comptes = recue.comptes;
      panneau.liste(comptes.length);
      // Liste beaucoup plus longue que la dernière acceptée : ta confirmation d'abord.
      await accepterListe(recue, panneau);
      journal((DRY ? 'Essai à blanc (DRY) : rien ne sera envoyé. ' : '')
        + 'Lancement : ' + compterRestants(lireEtat(), comptes) + ' comptes restants sur ' + comptes.length
        + ', plafond ' + PLAFOND + ', pause fixe ' + SECONDES_PAUSE + ' s.');
      panneau.statut('Vérification de la session…');
      majProgression();
      const contexte = await preparer();
      for (;;) {
        if (arretDemande) throw new Arret('utilisateur');
        etat = lireEtat();
        const suivant = prochainCompte(comptes, etat, vus);
        if (!suivant) break;
        if (n >= PLAFOND) throw new Arret('plafond');
        if (n > 0) {
          // Pause fixe entre deux comptes, pour rester sous les limites de débit.
          panneau.statut('Pause de ' + SECONDES_PAUSE + ' s (limite de débit de ' + LIBELLE + ').');
          await pause(PAUSE_MS);
          if (arretDemande) throw new Arret('utilisateur');
        }
        panneau.courant(suivant);
        panneau.statut(DRY ? 'Essai à blanc…' : 'Blocage en cours…');
        let resultat;
        if (DRY) {
          journal('[DRY] Bloquerait @' + suivant + ' (rien n\'est envoyé).');
          vus.add(cle(suivant));
          resultat = 'b';
        } else {
          resultat = await bloquer(contexte, suivant);
        }
        n += 1;
        if (resultat === 'b') bilan.bloques += 1;
        if (resultat === 'd') bilan.deja += 1;
        if (resultat === 'i') {
          bilan.introuvables += 1;
          serie.push(cle(suivant));
        } else {
          serie.length = 0;
        }
        if (!DRY) {
          etat = lireEtat();
          etat.faits[cle(suivant)] = resultat;
          ecrireEtat(etat);
        }
        if (INTROUVABLES_DE_SUITE_MAX > 0 && serie.length >= INTROUVABLES_DE_SUITE_MAX) {
          // Plusieurs comptes de suite introuvables : la requête ne répond
          // sans doute plus comme prévu. Ces comptes ne sont pas notés.
          etat = lireEtat();
          for (const k of serie) delete etat.faits[k];
          ecrireEtat(etat);
          bilan.introuvables -= serie.length;
          throw new Arret('introuvable', serie.length + ' comptes de suite introuvables (@' + serie.join(', @')
            + ') : la requête de recherche ne répond peut-être plus comme prévu. Ces comptes ne sont pas notés comme traités.');
        }
        majProgression();
      }
    } catch (e) {
      if (e instanceof Arret) {
        code = e.code;
        detail = e.detail;
        affiche = e.affiche;
      } else {
        code = 'erreur';
        detail = String((e && e.message) || e);
      }
    }
    etat = lireEtat();
    if (etat.lancement && etat.lancement.onglet === ONGLET) {
      etat.lancement = null;
      ecrireEtat(etat);
    }
    window[DRAPEAU] = false;
    majProgression();
    // Sans liste reçue, pas de bilan : rien n'a été tenté.
    panneau.fin(code, comptes ? bilan : null, detail, comptes ? compterRestants(etat, comptes) : undefined, affiche);
  }

  // =====================================================================
  // Encadré de lancement : rien ne se passe tant que tu ne cliques pas sur
  // son bouton (« Lancer », ou « Recevoir la liste et lancer » pour le
  // script console). Ce clic ouvre la petite fenêtre du site qui transmet
  // la liste : sans clic, le navigateur la bloquerait.
  // forcer : réafficher l'encadré (commande du menu, script collé de
  // nouveau), en retirant celui d'un lancement terminé.
  // =====================================================================

  function afficherLanceur(lancer, forcer) {
    if (forcer) {
      const ancien = document.getElementById('horsfil-lanceur');
      if (ancien) ancien.remove();
      const panneau = document.getElementById('horsfil-panneau');
      if (panneau && panneau.dataset.etat === 'fini') panneau.remove();
    }
    if (document.getElementById('horsfil-lanceur') || document.getElementById('horsfil-panneau')) return;
    const boite = noeud('div', Object.assign({}, STYLE_BOITE, { left: '16px', width: '300px', padding: '10px 12px' }));
    boite.id = 'horsfil-lanceur';
    boite.setAttribute('aria-label', 'Hors fil — ' + LIBELLE);
    const masquer = bouton('Masquer', 'masquer', false);
    masquer.addEventListener('click', () => boite.remove());
    // Stockage du gestionnaire absent : pas de bouton de lancement.
    if (!stockageDisponible()) {
      boite.dataset.refus = 'stockage';
      boite.appendChild(noeud('p', { margin: '0' }, 'Hors fil — ' + LIBELLE + ' : ' + MESSAGES.stockage));
      boite.appendChild(masquer);
      (document.body || document.documentElement).appendChild(boite);
      avertir(MESSAGES.stockage);
      return;
    }
    const etat = lireEtat();
    const ailleurs = lancementActifAilleurs(etat);
    const texte = ailleurs
      ? 'Hors fil — ' + LIBELLE + ' : un lancement est en cours dans un autre onglet.'
      : 'Hors fil — ' + LIBELLE + ' : bloquer sur ton compte les comptes de la liste'
        + (DRY ? ' (essai à blanc, DRY)' : '') + '.';
    boite.appendChild(noeud('p', { margin: '0' }, texte));
    if (!ailleurs) {
      // Le script ne connaît pas encore la liste : il ne montre que ce qu'il
      // a déjà traité dans ce navigateur.
      const traites = Object.keys(etat.faits).length;
      boite.appendChild(noeud('p', { margin: '4px 0 0', color: COULEURS.encre2, fontSize: '13px' },
        'Au clic, une petite fenêtre du site Hors fil s\'ouvre et transmet la liste à jour au script.'
        + (traites > 0 ? ' Déjà traités dans ce navigateur : ' + traites + '.' : '')));
      const b = bouton(SORTE === 'console' ? 'Recevoir la liste et lancer' : 'Lancer', 'lancer', true);
      b.addEventListener('click', () => {
        boite.remove();
        lancer();
      });
      boite.appendChild(b);
    }
    boite.appendChild(masquer);
    (document.body || document.documentElement).appendChild(boite);
  }

  // =====================================================================
  // Lancement : le script console affiche son encadré. Rien ne se passe
  // tant que tu ne cliques pas sur « Recevoir la liste et lancer » : sans
  // clic, le navigateur bloquerait la petite fenêtre du site.
  // =====================================================================
  journal('Version ' + VERSION + ' du ' + GENERE_LE + '. La liste est demandée au site Hors fil à chaque lancement.');
  if (location.origin !== ORIGINE) creerPanneau().fin('mauvais_site', null, 'page actuelle : ' + location.origin);
  else if (window[DRAPEAU]) avertir(MESSAGES.deja_en_cours);
  else afficherLanceur(executer, true);
})();

Userscript

Installer le userscript X. Ton gestionnaire de scripts affiche le code avant de l’installer.

Empreintes SHA-256

  • x.console.js : 9c774142b7a51c0ad43f96004807c2df0565d7097c6ad4186dcd5285511d4637
  • x.user.js : c5b074471b52401cf642fe8a6b7926398f846e57171cc76e246743b6a2d3fc2b

Instagram

vérifié sur page simulée

Bloque sur ton compte Instagram les comptes de la liste, un par un : pour chacun, il demande à Instagram le profil du compte, vérifie que son identifiant est exactement celui de la liste (sinon il s'arrête sans rien bloquer), puis envoie la requête de blocage du site web d'Instagram, depuis ta page et avec ta session. La liste n'est pas écrite dans le script : au lancement, il ouvre une petite fenêtre du site Hors fil, qui demande au site la liste à jour et la lui transmet. Il avance avec une pause fixe de 3,5 secondes entre deux comptes et au plus 40 comptes par lancement ; un nouveau lancement reprend là où le précédent s'est arrêté.

Où le lancer
Sur www.instagram.com, avec ton compte ouvert : colle instagram.console.js dans la console du navigateur (F12, onglet Console) puis clique sur « Recevoir la liste et lancer », ou installe instagram.user.js dans un gestionnaire de scripts (Violentmonkey, Tampermonkey) puis clique sur « Lancer » dans l'encadré Hors fil. Une petite fenêtre du site Hors fil s'ouvre alors et se referme seule ; si ton navigateur la bloque, autorise les fenêtres pop-up pour www.instagram.com.
Méthode
requêtes de la page
Comptes
778 dans la liste, mis à jour en direct
Par lancement
40 comptes au plus, une pause de 3,5 secondes entre deux
Version
2.0.0, vérifiée le 30 octobre 2026 sur page simulée

Ce qu’il ne fait pas

  • N'envoie aucune donnée ailleurs que chez Instagram et ne charge aucun code distant. Il n'envoie lui-même aucune requête au site Hors fil : il en ouvre seulement une page dans une petite fenêtre, et c'est cette page qui demande la liste au site.
  • N'accepte la liste que de la fenêtre du site qu'il vient d'ouvrir, et la refuse en entier si un seul identifiant n'a pas la forme d'un identifiant Instagram ; il n'exécute rien de ce qu'il reçoit.
  • Ne bloque rien, si la liste a beaucoup grossi depuis ton dernier lancement (plus de 10 % et plus de 200 comptes de plus), avant que tu confirmes : c'est une protection si le site Hors fil était compromis.
  • N'enregistre pas la liste : elle reste en mémoire le temps du lancement, et chaque lancement la redemande au site.
  • Ne garde sa progression (la liste des comptes déjà traités) que dans ton navigateur : le script console la garde dans le localStorage de la page de Instagram, que les scripts de Instagram peuvent lire ; le script utilisateur la garde dans le stockage de ton gestionnaire de scripts, hors de portée des pages de Instagram.
  • Ne suit, ne signale et ne contacte aucun compte ; le lien « Suivre @BlockFarRight » affiché à la fin n'est qu'un lien.
  • Ne contourne aucune limite : il s'arrête dès que Instagram signale une limite de débit.
  • Ne se lance pas si cette copie est trop ancienne pour la liste du site : il te demande alors de télécharger la nouvelle version sur https://onbloquelextremedroite.org/outils/.
  • N'a pas encore été essayé sur le vrai site Instagram, seulement sur une page simulée ; si Instagram a changé son site, il s'arrête avec un message.

Code pour la console

/*
 * Hors fil — Instagram
 * Script console, version 2.0.0, générée le 2026-10-31.
 * https://onbloquelextremedroite.org/outils/
 *
 * CE QU'IL FAIT
 * Lancé sur https://www.instagram.com avec ton compte ouvert, il bloque sur
 * ton compte les comptes de la liste Instagram de Hors fil, un par un. Pour
 * chaque compte, il demande à Instagram l'identifiant du compte (GET
 * /api/v1/users/web_profile_info/), vérifie que le profil renvoyé a
 * exactement l'identifiant demandé (sinon il s'arrête sans rien bloquer),
 * puis envoie la requête de blocage du site web d'Instagram (POST
 * /api/v1/web/friendships/<id>/block/), avec ta session.
 * - La liste n'est pas écrite dans ce fichier. Au lancement, pendant ton
 *   clic sur « Recevoir la liste et lancer », le script ouvre une petite
 *   fenêtre du site Hors fil
 *   (https://onbloquelextremedroite.org/outils/pont/), une nouvelle à chaque
 *   lancement. Quand elle est prête, le script lui demande la liste ; elle
 *   la demande alors au site et la transmet au script (postMessage), puis le
 *   script la ferme. Le script n'accepte que les messages de cette fenêtre,
 *   venus de https://onbloquelextremedroite.org et portant le jeton
 *   aléatoire tiré pour ce lancement (crypto.getRandomValues). Il refuse
 *   toute la liste si elle compte plus de 20 000 identifiants ou si un seul
 *   n'a pas exactement la forme d'un identifiant Instagram
 *   (MOTIF_IDENTIFIANT) ; il n'exécute rien de ce qu'il reçoit. L'encadré
 *   montre seulement le nombre de comptes reçus, jamais la liste. Il la
 *   garde en mémoire le temps du lancement, sans l'enregistrer ; chaque
 *   lancement la redemande.
 * - Version minimale : le site indique la plus ancienne version acceptée. Si
 *   cette copie est plus ancienne, le site ne lui transmet pas la liste ou
 *   le script la refuse : télécharge alors la nouvelle version sur
 *   https://onbloquelextremedroite.org/outils/.
 * - Changement brutal : le script retient, avec sa progression, la taille et
 *   le repère de la dernière liste acceptée. Si une nouvelle liste compte
 *   plus de 10 % et plus de 200 comptes de plus, il ne bloque rien avant ta
 *   confirmation (bouton « Confirmer et continuer ») : c'est une protection
 *   si le site Hors fil était compromis. Jamais au premier lancement.
 * - Pause fixe de 3,5 s entre deux comptes (PAUSE_MS) : c'est pour rester
 *   sous les limites de débit de Instagram.
 * - Au plus PLAFOND comptes par lancement (40 par défaut). Relance-le pour
 *   continuer.
 * - Progression gardée dans ce navigateur, dans le localStorage de la page
 *   de Instagram (clé « horsfil.instagram.v1 ») ; les scripts de Instagram
 *   peuvent la lire. Un nouveau lancement saute les comptes déjà traités.
 *   Pour tout recommencer, tape dans la console de https://www.instagram.com
 *   : localStorage.removeItem('horsfil.instagram.v1')
 * - Un encadré montre l'avancement, le compte en cours et un bouton
 *   « Arrêter ». À la fin, il affiche un bilan et un simple lien « Suivre
 *   @BlockFarRight » ; rien n'est suivi automatiquement.
 * - Il s'arrête avec un message clair : fenêtre du site bloquée, liste non
 *   reçue ou refusée, copie trop ancienne, pas de session, élément ou
 *   requête introuvable, limite de débit (HTTP 429 ou message de Instagram),
 *   plafond atteint, arrêt demandé.
 * - Il ne contacte que https://www.instagram.com. Il n'envoie lui-même
 *   aucune requête au site Hors fil : il ouvre seulement sa page de
 *   transmission de la liste, dans une petite fenêtre. Il n'envoie aucune
 *   donnée ailleurs et ne charge aucun code distant.
 * - DRY = true : essai à blanc ; il reçoit la liste comme d'habitude, puis
 *   affiche ce qu'il ferait, sans rien envoyer à Instagram ni enregistrer.
 *
 * COMMENT LE LANCER
 * Ouvre https://www.instagram.com avec ton compte. Ouvre la console du
 * navigateur (F12, puis l'onglet Console), colle tout le contenu de ce
 * fichier et appuie sur Entrée. Chrome peut d'abord te demander de taper
 * « allow pasting ». Un encadré « Hors fil » s'affiche : rien ne se passe
 * tant que tu ne cliques pas sur « Recevoir la liste et lancer ». Une petite
 * fenêtre du site Hors fil s'ouvre, transmet la liste et se referme ; si ton
 * navigateur la bloque, autorise les fenêtres pop-up pour www.instagram.com,
 * puis relance.
 *
 * RÉGLAGES
 * Juste en dessous : DRY, PLAFOND, PAUSE_MS.
 *
 * ESSAIS
 * Vérifié le 2026-10-30 sur une page simulée qui imite Instagram. Pas encore
 * essayé sur le vrai site : les repères peuvent avoir changé.
 *
 * SÉCURITÉ
 * Compare l'empreinte SHA-256 de ce fichier avec celle de la page des
 * outils. Ne colle et n'installe jamais du code qui ne vient pas de cette
 * page.
 */
(() => {
  'use strict';

  // =====================================================================
  // Réglages : tu peux les modifier avant de lancer.
  // =====================================================================

  // DRY = true : essai à blanc. Le script reçoit la liste, puis affiche dans
  // la console ce qu'il ferait, sans rien envoyer à Instagram ni enregistrer.
  // Par défaut : false (il bloque).
  const DRY = false;

  // PLAFOND : nombre maximal de comptes traités par lancement. Relance le
  // script pour continuer : les comptes déjà traités sont sautés.
  const PLAFOND = 40;

  // PAUSE_MS : pause fixe entre deux comptes, en millisecondes, pour rester
  // sous les limites de débit de Instagram.
  const PAUSE_MS = 3500;

  // =====================================================================
  // Données, écrites à la génération. La liste des comptes n'y est pas :
  // au lancement, une petite fenêtre du site la transmet au script (voir
  // « Réception de la liste », plus bas).
  // =====================================================================

  const VERSION = "2.0.0";
  const GENERE_LE = "2026-10-31";
  const SITE_OUTILS = "https://onbloquelextremedroite.org/outils/";
  // Site Hors fil : seule origine dont le script accepte la liste, et page
  // « pont » qu'il ouvre au lancement, dans une petite fenêtre, pour la recevoir.
  const SITE_ORIGINE = "https://onbloquelextremedroite.org";
  const SITE_PONT = "https://onbloquelextremedroite.org/outils/pont/";
  // Attente maximale de la liste, en millisecondes.
  const DELAI_PONT_MS = 90000;
  // « userscript » : progression dans le stockage du gestionnaire de scripts ;
  // « console » : dans le localStorage de la page.
  const SORTE = "console";
  const PLATEFORME = "instagram";
  const LIBELLE = "Instagram";
  const ORIGINE = "https://www.instagram.com";
  const CLE = "horsfil.instagram.v1";
  const SUIVRE_URL = "https://x.com/intent/follow?screen_name=BlockFarRight";

  // Forme d'un identifiant Instagram. La liste reçue est refusée en entier
  // si un seul identifiant ne correspond pas exactement à ce motif.
  const MOTIF_IDENTIFIANT = /^(?:[A-Za-z0-9._]{1,30})$/;

  // =====================================================================
  // Fonctions communes. Ne modifie rien en dessous de cette ligne.
  // =====================================================================

  const PREFIXE = '[Hors fil — ' + LIBELLE + ']';
  const SECONDES_PAUSE = String(PAUSE_MS / 1000).replace('.', ',');
  // Un lancement qui ne donne plus signe de vie depuis 30 s est considéré
  // comme terminé (onglet fermé, page rechargée).
  const BATTEMENT_PERIME_MS = 30000;
  const DRAPEAU = '__horsfil_' + PLATEFORME;
  // Une liste reçue compte de 1 à IDENTIFIANTS_MAX identifiants.
  const IDENTIFIANTS_MAX = 20000;
  const COULEURS = {
    fond: '#F7F5F0',
    encre: '#111317',
    encre2: '#4A4F57',
    accent: '#0E6E78',
    teinte: '#DCEFEF',
  };

  // Raisons d'arrêt, affichées dans l'encadré et dans la console.
  const MESSAGES = {
    termine: 'Terminé : tous les comptes de la liste ' + LIBELLE + ' ont été traités.',
    plafond: 'Plafond atteint : ' + PLAFOND + ' comptes pour ce lancement. Relance plus tard pour continuer ; les comptes déjà traités seront sautés.',
    utilisateur: 'Arrêté à ta demande. Relance quand tu veux ; les comptes déjà traités seront sautés.',
    non_connecte: 'Arrêt : aucune session ' + LIBELLE + ' active. Connecte-toi à ton compte ' + LIBELLE + ', puis relance.',
    limite: 'Arrêt : ' + LIBELLE + ' signale une limite de débit. Attends quelques heures avant de relancer ; les comptes déjà traités seront sautés.',
    introuvable: 'Arrêt : élément ou requête introuvable. ' + LIBELLE + ' a peut-être changé son site ; le script ne va pas plus loin.',
    mauvais_site: 'Ce script se lance sur ' + ORIGINE + ', pas sur cette page.',
    deja_en_cours: 'Un lancement est déjà en cours, dans cet onglet ou dans un autre.',
    erreur: 'Arrêt : erreur inattendue. Rien d\'autre n\'a été envoyé.',
    stockage: 'Arrêt : ton gestionnaire de scripts ne donne pas accès à son stockage (GM_getValue, GM_setValue) ; le script ne peut pas garder sa progression. Utilise Violentmonkey ou Tampermonkey.',
    mauvais_compte: 'Arrêt de sécurité : la réponse de ' + LIBELLE + ' ne correspond pas au compte demandé. Le script s\'arrête pour ne pas bloquer un autre compte.',
    clic_refuse: 'Arrêt de sécurité : l\'élément trouvé porte un libellé interdit (signaler, envoyer, supprimer, ne plus suivre…). Le script ne clique jamais sur un tel élément : il n\'a pas cliqué. ' + LIBELLE + ' a peut-être changé son site.',
    formulaire: 'Arrêt de sécurité : une boîte de signalement est ouverte. Ferme-la sans rien envoyer ; le script ne remplit et n\'envoie jamais de formulaire.',
    non_verifie: 'Arrêt : le blocage de deux comptes de suite n\'a pas pu être vérifié sur la page. Vérifie ces comptes à la main ; ' + LIBELLE + ' a peut-être changé son site.',
    sans_reponse: 'Arrêt : le script attendait ton clic et ne l\'a pas vu venir. Relance quand tu veux ; les comptes déjà traités seront sautés.',
    // Réception de la liste (textes du protocole du site).
    pont_bloque: 'Ton navigateur a bloqué la fenêtre du site Hors fil. Autorise les fenêtres pop-up pour ' + location.host + ', puis relance.',
    pont_delai: 'La liste n\'est pas arrivée. Vérifie que la fenêtre du site s\'est ouverte, puis relance.',
    trop_ancienne: 'Cette copie de l\'outil est trop ancienne. Télécharge la nouvelle version sur ' + SITE_OUTILS + '.',
    pont_erreur: 'Arrêt : le site Hors fil n\'a pas transmis la liste. Réessaie plus tard.',
    liste_invalide: 'Arrêt de sécurité : la liste reçue n\'a pas la forme attendue. Le script ne s\'en sert pas : rien n\'a été bloqué.',
    liste_absente: 'Arrêt : la liste reçue pour ce lancement n\'est plus là (effacée, ou reçue il y a plus de 24 heures). Relance pour la recevoir de nouveau ; les comptes déjà traités seront sautés.',
  };

  let arretDemande = false;

  function journal(...morceaux) {
    console.info(PREFIXE, ...morceaux);
  }

  function avertir(...morceaux) {
    console.warn(PREFIXE, ...morceaux);
  }

  function dormir(ms) {
    return new Promise((suite) => setTimeout(suite, ms));
  }

  function cle(identifiant) {
    return String(identifiant).toLowerCase();
  }

  // Un compte est soit un identifiant, soit [identifiant, chemin du profil].
  function identifiant(compte) {
    return Array.isArray(compte) ? compte[0] : compte;
  }

  // Espaces insécables de la typographie française.
  function typo(texte) {
    return String(texte).replace(/ ([:;!?»])/g, ' $1').replace(/« /g, '« ');
  }

  function lireCookie(nom) {
    for (const morceau of document.cookie.split(';')) {
      const i = morceau.indexOf('=');
      if (i < 0) continue;
      if (morceau.slice(0, i).trim() !== nom) continue;
      const valeur = morceau.slice(i + 1).trim();
      try {
        return decodeURIComponent(valeur);
      } catch (e) {
        return valeur;
      }
    }
    return '';
  }

  // Raison d'arrêt. affiche : texte à montrer à la place du message
  // habituel (par exemple, le message transmis par le site).
  class Arret extends Error {
    constructor(code, detail, affiche) {
      super(detail || code);
      this.code = code;
      this.detail = detail || '';
      this.affiche = affiche || '';
    }
  }

  // ---------------------------------------------------------------------
  // Progression, gardée dans ce navigateur sous la clé CLE :
  // - script utilisateur : dans le stockage du gestionnaire de scripts
  //   (GM_getValue, GM_setValue), que les pages de la plateforme ne peuvent
  //   pas lire ;
  // - script console : dans le localStorage de la page de la plateforme
  //   (seul stockage possible pour du code collé dans la console ; les
  //   scripts de la plateforme peuvent le lire).
  // faits : { identifiant en minuscules : 'b' (bloqué), 'd' (l'était déjà),
  //           'i' (non trouvé), 'n' (blocage non vérifié) }
  // lancement : le lancement en cours (onglet, battement, compteurs).
  // derniere : { liste, n }, repère et taille de la dernière liste acceptée
  // (voir « Changement brutal de la liste »).
  // En mode DRY, rien n'est écrit. La liste elle-même n'est jamais écrite
  // ici (voir « Réception de la liste »).
  // ---------------------------------------------------------------------

  function stockageGestionnaire() {
    return typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
  }

  // Faux seulement pour un script utilisateur dont le gestionnaire ne donne
  // pas accès à son stockage : il ne se lance pas.
  function stockageDisponible() {
    return SORTE !== 'userscript' || stockageGestionnaire();
  }

  function lireBrut() {
    if (SORTE === 'userscript') {
      if (!stockageGestionnaire()) return null;
      const v = GM_getValue(CLE, null);
      return typeof v === 'string' ? v : null;
    }
    return localStorage.getItem(CLE);
  }

  function ecrireBrut(texte) {
    if (SORTE === 'userscript') {
      if (stockageGestionnaire()) GM_setValue(CLE, texte);
      return;
    }
    localStorage.setItem(CLE, texte);
  }

  function effacerBrut() {
    if (SORTE === 'userscript') {
      if (typeof GM_deleteValue === 'function') GM_deleteValue(CLE);
      else if (stockageGestionnaire()) GM_setValue(CLE, null);
      return;
    }
    localStorage.removeItem(CLE);
  }

  function etatVide() {
    return { v: 1, faits: {}, lancement: null };
  }

  function lireEtat() {
    let brut = null;
    try {
      brut = lireBrut();
    } catch (e) {
      brut = null;
    }
    let etat = null;
    try {
      etat = brut ? JSON.parse(brut) : null;
    } catch (e) {
      etat = null;
    }
    if (!etat || etat.v !== 1 || typeof etat.faits !== 'object' || etat.faits === null) {
      return etatVide();
    }
    if (typeof etat.lancement !== 'object') etat.lancement = null;
    return etat;
  }

  function ecrireEtat(etat) {
    if (DRY) return;
    try {
      ecrireBrut(JSON.stringify(etat));
    } catch (e) {
      avertir('Impossible d\'enregistrer la progression :', e);
    }
  }

  // Commande du gestionnaire de scripts : tout recommencer.
  function effacerProgression() {
    const etat = lireEtat();
    if (etat.lancement && etat.lancement.actif && Date.now() - (etat.lancement.battement || 0) < BATTEMENT_PERIME_MS) {
      avertir('Un lancement est en cours : arrête-le avant d\'effacer la progression.');
      return;
    }
    if (typeof confirm === 'function' && !confirm(typo('Hors fil — ' + LIBELLE + ' : effacer la progression ? Les comptes déjà traités seront de nouveau proposés.'))) return;
    try {
      effacerBrut();
      // Outils par l'interface : la liste gardée part aussi.
      if (typeof effacerListeGardee === 'function') effacerListeGardee();
      journal('Progression effacée.');
    } catch (e) {
      avertir('Impossible d\'effacer la progression :', e);
    }
  }

  // Comptes de la liste reçue qui restent à traiter.
  function compterRestants(etat, comptes) {
    return comptes.filter((c) => !etat.faits[cle(identifiant(c))]).length;
  }

  // Identifiant aléatoire de cet onglet, sans aucune donnée : il sert
  // seulement à savoir quel onglet mène le lancement en cours (pas deux
  // lancements à la fois, reprise dans le bon onglet). Il est gardé dans le
  // sessionStorage de la page (clé « horsfil.onglet »), qui survit aux
  // changements de page dans le même onglet, mais pas d'un onglet à l'autre.
  function lireIdOnglet() {
    const nom = 'horsfil.onglet';
    try {
      let id = sessionStorage.getItem(nom);
      if (!id) {
        id = Math.random().toString(36).slice(2) + Date.now().toString(36);
        sessionStorage.setItem(nom, id);
      }
      return id;
    } catch (e) {
      return Math.random().toString(36).slice(2) + Date.now().toString(36);
    }
  }
  const ONGLET = lireIdOnglet();

  function lancementActifAilleurs(etat) {
    const l = etat.lancement;
    return !!(l && l.actif && l.onglet !== ONGLET && Date.now() - (l.battement || 0) < BATTEMENT_PERIME_MS);
  }

  let dernierBattement = 0;
  function battre() {
    if (DRY) return;
    const maintenant = Date.now();
    if (maintenant - dernierBattement < 2000) return;
    dernierBattement = maintenant;
    const etat = lireEtat();
    if (etat.lancement && etat.lancement.actif && etat.lancement.onglet === ONGLET) {
      etat.lancement.battement = maintenant;
      ecrireEtat(etat);
    }
  }

  // Pause fixe, interrompue dès que tu cliques sur « Arrêter ».
  async function pause(ms) {
    const fin = Date.now() + ms;
    while (Date.now() < fin) {
      if (arretDemande) return;
      await dormir(Math.min(100, fin - Date.now()));
      battre();
    }
  }

  // ---------------------------------------------------------------------
  // Réception de la liste. La liste n'est pas écrite dans ce script : au
  // lancement, pendant ton clic, le script ouvre une petite fenêtre du site
  // Hors fil (la page « pont », SITE_PONT), une nouvelle à chaque lancement.
  // Quand elle est prête (message horsfil-pret), le script lui demande la
  // liste (horsfil-demande) : c'est seulement alors qu'elle la demande au
  // site. Elle la transmet au script par postMessage ; le script répond
  // « reçu » (horsfil-recu) et ferme la fenêtre.
  // Le script n'envoie lui-même aucune requête au site : l'adresse de la
  // fenêtre porte seulement, après « # » (partie qui ne part pas vers le
  // serveur), la plateforme, l'origine de cette page, un jeton aléatoire et
  // la version du script.
  //
  // Le script ignore tout message qui ne vient pas exactement de
  // SITE_ORIGINE, de la fenêtre qu'il vient d'ouvrir, ou qui ne porte pas
  // le jeton tiré pour cette ouverture. Il refuse toute la liste (rien n'est
  // bloqué) si elle ne vise pas cette plateforme, si elle compte 0 ou plus
  // de IDENTIFIANTS_MAX identifiants, ou si un seul identifiant n'a pas
  // exactement la forme MOTIF_IDENTIFIANT. La liste n'est qu'une liste
  // d'identifiants : rien de ce qu'elle contient n'est exécuté.
  // ---------------------------------------------------------------------

  const MOTIF_VERSION = /^(\d{1,9})\.(\d{1,9})\.(\d{1,9})$/;
  // Repère de la liste transmise par le site : 12 caractères hexadécimaux.
  const MOTIF_REPERE_LISTE = /^[0-9a-fA-F]{12}$/;

  // Jeton de 32 caractères hexadécimaux minuscules, tiré au hasard
  // (crypto.getRandomValues) à chaque ouverture de la fenêtre.
  function tirerJeton() {
    const octets = new Uint8Array(16);
    crypto.getRandomValues(octets);
    return [...octets].map((o) => o.toString(16).padStart(2, '0')).join('');
  }

  // Un objet ordinaire ({…}) : ni tableau, ni texte, ni autre sorte d'objet.
  function objetOrdinaire(v) {
    if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
    if (Object.prototype.toString.call(v) !== '[object Object]') return false;
    const proto = Object.getPrototypeOf(v);
    return proto === null || Object.getPrototypeOf(proto) === null;
  }

  // [X, Y, Z] pour « X.Y.Z », sinon null.
  function nombresVersion(v) {
    const m = MOTIF_VERSION.exec(typeof v === 'string' ? v : '');
    return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
  }

  // Compare deux versions nombre par nombre (1.10.0 vient après 1.9.0) :
  // négatif si a < b, 0 si égales, positif si a > b.
  function comparerVersions(a, b) {
    for (let i = 0; i < 3; i++) {
      if (a[i] !== b[i]) return a[i] - b[i];
    }
    return 0;
  }

  // Ouvre la page pont. À appeler pendant ton clic et avant toute attente :
  // sans clic, le navigateur bloque la fenêtre.
  function ouvrirPont() {
    const pont = { jeton: '', fenetre: null, ouverte: Date.now(), reponse: null, ecouteur: null, erreur: '' };
    try {
      pont.jeton = tirerJeton();
    } catch (e) {
      pont.erreur = 'tirage du jeton impossible (crypto.getRandomValues) : ' + ((e && e.message) || e);
      return pont;
    }
    // Contrôles de chaque message : origine, source, objet ordinaire, jeton.
    // « Prête » reçoit la demande de la liste ; seule la première réponse
    // (liste ou erreur) est gardée.
    pont.ecouteur = (ev) => {
      if (pont.reponse) return;
      if (ev.origin !== SITE_ORIGINE) return;
      if (!pont.fenetre || ev.source !== pont.fenetre) return;
      const d = ev.data;
      if (!objetOrdinaire(d) || d.n !== pont.jeton) return;
      if (d.type === 'horsfil-pret') {
        // La page pont ne demande la liste au site qu'après cette réponse.
        try {
          ev.source.postMessage({ type: 'horsfil-demande', n: pont.jeton }, SITE_ORIGINE);
          journal('Fenêtre du site prête : demande de la liste envoyée.');
        } catch (e) {
          avertir('Demande de la liste non transmise à la fenêtre du site :', e);
        }
        return;
      }
      if (d.type !== 'horsfil-liste' && d.type !== 'horsfil-erreur') return;
      pont.reponse = { donnees: d, source: ev.source };
    };
    window.addEventListener('message', pont.ecouteur);
    const adresse = SITE_PONT + '#p=' + PLATEFORME + '&o=' + encodeURIComponent(location.origin)
      + '&n=' + pont.jeton + '&v=' + VERSION;
    try {
      // Un nom de fenêtre par lancement : jamais une ancienne fenêtre reprise.
      pont.fenetre = window.open(adresse, 'horsfil-pont-' + pont.jeton, 'popup=yes,width=480,height=640');
    } catch (e) {
      pont.fenetre = null;
    }
    if (!pont.fenetre) window.removeEventListener('message', pont.ecouteur);
    return pont;
  }

  // Ferme la fenêtre du site, au mieux : juste après l'accusé de réception,
  // et quand le script renonce à la liste (refus, délai dépassé, arrêt).
  function fermerPont(pont) {
    try {
      if (pont.fenetre && !pont.fenetre.closed) pont.fenetre.close();
    } catch (e) {
      // fenêtre déjà fermée
    }
  }

  // Texte transmis par le site pour être affiché : une ligne, sans mise en
  // forme (il n'est jamais interprété comme du code ou du HTML).
  function texteDuSite(t) {
    return typeof t === 'string' ? t.replace(/\s+/g, ' ').trim().slice(0, 300) : '';
  }

  // Contrôles de la liste, avant tout blocage. Un seul échec arrête le
  // lancement, sans rien bloquer. Renvoie { liste, comptes } : comptes sans
  // doublons (sans tenir compte de la casse), dans l'ordre reçu.
  function verifierListe(d) {
    if (d.plateforme !== PLATEFORME) {
      throw new Arret('liste_invalide', 'liste prévue pour une autre plateforme (« '
        + String(d.plateforme).slice(0, 20) + ' » au lieu de « ' + PLATEFORME + ' »)');
    }
    // Version d'abord : une copie trop ancienne ne reconnaît peut-être plus
    // les identifiants de la liste.
    const minimum = nombresVersion(d.outil_min);
    if (!minimum) throw new Arret('liste_invalide', 'version minimale illisible');
    if (comparerVersions(nombresVersion(VERSION), minimum) < 0) {
      throw new Arret('trop_ancienne', 'version ' + VERSION + ', version minimale demandée par le site : ' + d.outil_min);
    }
    if (typeof d.liste !== 'string' || !MOTIF_REPERE_LISTE.test(d.liste)) {
      throw new Arret('liste_invalide', 'repère de la liste illisible');
    }
    const recus = d.comptes;
    if (!Array.isArray(recus) || recus.length < 1 || recus.length > IDENTIFIANTS_MAX) {
      throw new Arret('liste_invalide', Array.isArray(recus)
        ? recus.length + ' identifiants reçus, alors qu\'il en faut de 1 à ' + IDENTIFIANTS_MAX
        : 'pas de tableau d\'identifiants');
    }
    const comptes = [];
    const vus = new Set();
    for (let i = 0; i < recus.length; i++) {
      const h = recus[i];
      if (typeof h !== 'string' || !MOTIF_IDENTIFIANT.test(h)) {
        throw new Arret('liste_invalide', 'l\'identifiant n° ' + (i + 1) + ' sur ' + recus.length
          + ' n\'a pas la forme d\'un identifiant ' + LIBELLE + ' ; toute la liste est refusée');
      }
      if (vus.has(cle(h))) continue;
      vus.add(cle(h));
      comptes.push(h);
    }
    return { liste: d.liste, comptes };
  }

  // Attend la réponse de la page pont (DELAI_PONT_MS au plus, depuis
  // l'ouverture), vérifie la liste, envoie l'accusé de réception et ferme
  // la fenêtre (elle se ferme aussi d'elle-même). Renvoie { liste, comptes },
  // ou lève un Arret (rien n'est bloqué).
  async function recevoirListe(pont, panneau) {
    if (pont.erreur) throw new Arret('erreur', pont.erreur);
    if (!pont.fenetre) throw new Arret('pont_bloque', 'window.open n\'a pas ouvert la fenêtre');
    panneau.statut('Une petite fenêtre du site Hors fil s\'est ouverte : elle demande la liste au site et la transmet au script…');
    journal('Fenêtre du site Hors fil ouverte (' + SITE_PONT + ') : attente de la liste.');
    const fin = pont.ouverte + DELAI_PONT_MS;
    try {
      while (!pont.reponse) {
        if (arretDemande) throw new Arret('utilisateur');
        if (Date.now() >= fin) {
          throw new Arret('pont_delai', 'aucune réponse de la fenêtre du site en ' + Math.round(DELAI_PONT_MS / 1000) + ' s');
        }
        await dormir(100);
        battre();
      }
      const d = pont.reponse.donnees;
      if (d.type === 'horsfil-erreur') {
        const erreur = typeof d.erreur === 'string' ? d.erreur.slice(0, 20) : '';
        throw new Arret(erreur === 'version' ? 'trop_ancienne' : 'pont_erreur',
          'erreur « ' + (erreur || '?') + ' » signalée par le site', texteDuSite(d.message));
      }
      const recue = verifierListe(d);
      // Accusé de réception ; la fenêtre est fermée juste après (finally).
      try {
        pont.reponse.source.postMessage({ type: 'horsfil-recu', n: pont.jeton }, SITE_ORIGINE);
      } catch (e) {
        avertir('Accusé de réception non transmis à la fenêtre du site :', e);
      }
      journal('Liste reçue du site : ' + recue.comptes.length + ' comptes (liste ' + recue.liste + ').');
      return recue;
    } finally {
      window.removeEventListener('message', pont.ecouteur);
      fermerPont(pont);
    }
  }

  // ---------------------------------------------------------------------
  // Changement brutal de la liste. Le script retient, dans sa progression
  // (champ derniere : { liste, n }), le repère et la taille de la dernière
  // liste acceptée. Si une nouvelle liste compte plus de max(n × 1,10 ;
  // n + 200) comptes, il ne bloque rien avant ta confirmation : si le site
  // Hors fil était compromis, une liste gonflée ne passerait pas sans que tu
  // le voies. Jamais au premier lancement (aucune liste retenue).
  // ---------------------------------------------------------------------

  // Taille de la dernière liste acceptée si la nouvelle la dépasse
  // brutalement, sinon null.
  function changementBrutal(etat, nombre) {
    const d = etat.derniere;
    if (!d || typeof d !== 'object' || !Number.isInteger(d.n) || d.n < 1) return null;
    // nombre > max(n × 1,1 ; n + 200), compté en entiers.
    return (nombre * 10 > d.n * 11 && nombre > d.n + 200) ? d.n : null;
  }

  // Avant tout blocage : ta confirmation si la liste a beaucoup changé,
  // puis cette liste devient la dernière acceptée (rien n'est écrit en DRY).
  async function accepterListe(recue, panneau) {
    const avant = changementBrutal(lireEtat(), recue.comptes.length);
    if (avant !== null) {
      const texte = 'La liste a beaucoup changé depuis ton dernier lancement : ' + recue.comptes.length
        + ' comptes au lieu de ' + avant + '. Vérifie sur la page des outils que tout est normal, puis confirme.';
      avertir(texte);
      if (!(await panneau.confirmer(texte))) throw new Arret('utilisateur');
      journal('Changement de la liste confirmé : ' + recue.comptes.length + ' comptes au lieu de ' + avant + '.');
    }
    const etat = lireEtat();
    etat.derniere = { liste: recue.liste, n: recue.comptes.length };
    ecrireEtat(etat);
  }

  // ---------------------------------------------------------------------
  // Encadré affiché sur la page (construit avec des nœuds DOM, sans HTML).
  // ---------------------------------------------------------------------

  function noeud(balise, styles, texte) {
    const e = document.createElement(balise);
    if (styles) Object.assign(e.style, styles);
    if (texte !== undefined && texte !== null) e.textContent = typo(texte);
    return e;
  }

  function bouton(texte, action, principal) {
    const b = noeud('button', {
      font: 'inherit',
      fontWeight: '700',
      minHeight: '44px',
      padding: '8px 14px',
      margin: '8px 8px 0 0',
      borderRadius: '6px',
      border: '2px solid ' + COULEURS.accent,
      cursor: 'pointer',
      background: principal ? COULEURS.accent : COULEURS.fond,
      color: principal ? COULEURS.fond : COULEURS.accent,
    }, texte);
    b.type = 'button';
    b.dataset.action = action;
    return b;
  }

  function lienOutils() {
    const lien = noeud('a', { display: 'inline-block', marginTop: '6px', color: COULEURS.accent, fontWeight: '700', textDecoration: 'underline' }, 'Page des outils Hors fil');
    lien.href = SITE_OUTILS;
    lien.target = '_blank';
    lien.rel = 'noopener noreferrer';
    return lien;
  }

  const STYLE_BOITE = {
    position: 'fixed',
    bottom: '16px',
    zIndex: '2147483647',
    boxSizing: 'border-box',
    maxWidth: 'calc(100vw - 32px)',
    padding: '14px 16px',
    background: COULEURS.fond,
    color: COULEURS.encre,
    border: '2px solid ' + COULEURS.accent,
    borderRadius: '8px',
    boxShadow: '0 6px 24px rgba(0, 0, 0, 0.25)',
    font: '15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
    textAlign: 'left',
  };

  function demanderArret(panneau) {
    if (arretDemande) return;
    arretDemande = true;
    journal('Arrêt demandé.');
    const etat = lireEtat();
    if (etat.lancement && etat.lancement.onglet === ONGLET) {
      etat.lancement.actif = false;
      ecrireEtat(etat);
    }
    // Outils par l'interface : la liste gardée est effacée dès l'arrêt.
    if (typeof effacerListeGardee === 'function') effacerListeGardee();
    panneau.statut('Arrêt demandé…');
  }

  function creerPanneau() {
    for (const id of ['horsfil-panneau', 'horsfil-lanceur']) {
      const ancien = document.getElementById(id);
      if (ancien) ancien.remove();
    }
    const racine = noeud('section', Object.assign({}, STYLE_BOITE, {
      right: '16px',
      width: '340px',
      maxHeight: '80vh',
      overflow: 'auto',
    }));
    racine.id = 'horsfil-panneau';
    racine.setAttribute('aria-label', 'Hors fil — ' + LIBELLE);
    racine.dataset.etat = 'en_cours';
    const elTitre = noeud('p', { margin: '0 0 6px', fontWeight: '700', fontSize: '16px' },
      'Hors fil — ' + LIBELLE + (DRY ? ' · essai à blanc (DRY)' : ''));
    // Nombre de comptes reçus du site : la liste elle-même n'est jamais affichée.
    const elListe = noeud('p', { margin: '0 0 4px', display: 'none' }, '');
    const elProgression = noeud('p', { margin: '0 0 4px' }, '');
    const elCourant = noeud('p', { margin: '0 0 4px', fontFamily: 'ui-monospace, Menlo, Consolas, monospace' }, '');
    const elStatut = noeud('p', { margin: '0 0 4px', color: COULEURS.encre2 }, 'Préparation…');
    elStatut.setAttribute('role', 'status');
    elStatut.setAttribute('aria-live', 'polite');
    const elDetail = noeud('p', { margin: '4px 0 0', color: COULEURS.encre2, fontSize: '13px', display: 'none' }, '');
    // Consigne quand le script attend un clic de ta part (YouTube).
    const elAttente = noeud('p', { margin: '6px 0 0', padding: '8px 10px', fontWeight: '700', border: '2px solid ' + COULEURS.accent, borderRadius: '6px', display: 'none' }, '');
    elAttente.setAttribute('role', 'alert');
    // Demande de confirmation quand la liste a beaucoup changé.
    const elConfirmation = noeud('div', { margin: '6px 0 0', padding: '8px 10px', border: '2px solid ' + COULEURS.accent, borderRadius: '6px', display: 'none' });
    elConfirmation.setAttribute('role', 'alert');
    const elConfirmer = bouton('Confirmer et continuer', 'confirmer', true);
    let confirme = false;
    elConfirmer.addEventListener('click', () => {
      confirme = true;
    });
    const elBilan = noeud('div', { display: 'none', marginTop: '8px', padding: '8px 10px', background: COULEURS.teinte, borderRadius: '6px' });
    const elArreter = bouton('Arrêter', 'arreter', true);
    const elFermer = bouton('Fermer', 'fermer', false);
    elFermer.style.display = 'none';
    racine.append(elTitre, elListe, elProgression, elCourant, elStatut, elAttente, elConfirmation, elDetail, elBilan, elArreter, elFermer);
    (document.body || document.documentElement).appendChild(racine);

    const panneau = {
      racine,
      liste(nombre) {
        racine.dataset.comptes = String(nombre);
        elListe.textContent = typo('Liste reçue du site : ' + nombre + (nombre > 1 ? ' comptes.' : ' compte.'));
        elListe.style.display = 'block';
      },
      progression(n, traites, total) {
        elProgression.textContent = typo((DRY ? 'Essai à blanc : ' : 'Ce lancement : ') + n + ' sur ' + PLAFOND
          + ' · Liste : ' + traites + ' sur ' + total + ' traités');
      },
      courant(h) {
        elCourant.textContent = h ? 'Compte en cours : @' + h : '';
      },
      statut(texte) {
        elStatut.textContent = typo(texte);
      },
      attente(texte) {
        if (texte) {
          racine.dataset.attente = 'manuel';
          elAttente.textContent = typo(texte);
          elAttente.style.display = 'block';
        } else {
          delete racine.dataset.attente;
          elAttente.textContent = '';
          elAttente.style.display = 'none';
        }
      },
      // Montre texte et attend ta réponse : vrai si tu confirmes, faux si tu
      // arrêtes. Rien n'est bloqué pendant l'attente.
      async confirmer(texte) {
        confirme = false;
        racine.dataset.attente = 'confirmation';
        const action = noeud('div', { marginTop: '2px' });
        action.appendChild(elConfirmer);
        elConfirmation.replaceChildren(noeud('p', { margin: '0', fontWeight: '700' }, texte), lienOutils(), action);
        elConfirmation.style.display = 'block';
        elStatut.textContent = typo('En attente de ta confirmation : rien n\'est bloqué pour l\'instant.');
        try {
          for (;;) {
            if (arretDemande) return false;
            if (confirme) return true;
            await dormir(100);
            battre();
          }
        } finally {
          delete racine.dataset.attente;
          elConfirmation.replaceChildren();
          elConfirmation.style.display = 'none';
        }
      },
      // affiche : texte à montrer à la place du message habituel (message
      // transmis par le site).
      fin(code, bilan, detail, restants, affiche) {
        racine.dataset.etat = 'fini';
        racine.dataset.raison = code;
        delete racine.dataset.attente;
        elAttente.style.display = 'none';
        elConfirmation.style.display = 'none';
        elCourant.textContent = '';
        const message = affiche || MESSAGES[code] || MESSAGES.erreur;
        elStatut.textContent = typo(message);
        elStatut.style.color = COULEURS.encre;
        elStatut.style.fontWeight = '700';
        if (detail) {
          elDetail.textContent = typo('Détail : ' + detail);
          elDetail.style.display = 'block';
        }
        if (bilan) {
          elBilan.replaceChildren();
          const lignes = DRY
            ? ['Essai à blanc : rien n\'a été envoyé.', 'Comptes qui auraient été traités : ' + bilan.bloques]
            : ['Bloqués pendant ce lancement : ' + bilan.bloques,
              'Bloqués avant ce lancement : ' + bilan.deja,
              'Non trouvés, sautés : ' + bilan.introuvables];
          if (!DRY && typeof bilan.nonVerifies === 'number') {
            lignes.push('Blocage non vérifié (à contrôler à la main) : ' + bilan.nonVerifies);
          }
          if (typeof restants === 'number') lignes.push('Restants dans la liste : ' + restants);
          for (const ligne of lignes) elBilan.appendChild(noeud('p', { margin: '0 0 2px' }, ligne));
          const lien = noeud('a', {
            display: 'inline-block',
            marginTop: '6px',
            color: COULEURS.accent,
            fontWeight: '700',
            textDecoration: 'underline',
          }, 'Suivre @BlockFarRight');
          lien.href = SUIVRE_URL;
          lien.target = '_blank';
          lien.rel = 'noopener noreferrer';
          elBilan.appendChild(lien);
          elBilan.style.display = 'block';
        }
        elArreter.style.display = 'none';
        elFermer.style.display = 'inline-block';
        const ecrire = (code === 'termine' || code === 'plafond' || code === 'utilisateur') ? journal : avertir;
        ecrire(message + (detail ? ' (' + detail + ')' : ''));
        if (code === 'trop_ancienne') {
          elBilan.replaceChildren(lienOutils());
          elBilan.style.display = 'block';
        }
        if (bilan) {
          journal('Bilan : ' + bilan.bloques + ' bloqués, ' + bilan.deja + ' bloqués avant, '
            + bilan.introuvables + ' non trouvés'
            + (typeof bilan.nonVerifies === 'number' ? ', ' + bilan.nonVerifies + ' non vérifiés' : '')
            + (typeof restants === 'number' ? ', ' + restants + ' restants.' : '.'));
        }
      },
    };
    elArreter.addEventListener('click', () => demanderArret(panneau));
    elFermer.addEventListener('click', () => racine.remove());
    return panneau;
  }

  // =====================================================================
  // Instagram : la page demande à son propre site l'identifiant du compte,
  // puis envoie la requête de blocage, avec ta session (cookies).
  // =====================================================================

  // Une réponse « introuvable » d'Instagram ne dit pas toujours pourquoi :
  // au troisième compte introuvable de suite, le script s'arrête sans noter
  // ces comptes, au cas où la requête aurait changé.
  const INTROUVABLES_DE_SUITE_MAX = 3;
  // Identifiant d'application que la page d'Instagram envoie avec ses
  // requêtes (en-tête x-ig-app-id). Il est lu dans la page à chaque lancement.
  const MOTIFS_APP_ID = [
    /"X-IG-App-ID"\s*:\s*"(\d{6,20})"/,
    /"appId"\s*:\s*"(\d{6,20})"/,
    /"app_id"\s*:\s*"(\d{6,20})"/,
    /"APP_ID"\s*:\s*"(\d{6,20})"/,
  ];
  const MOTIF_LIMITE = /feedback_required|wait a few minutes|try again later|patiente|réessa|too many/;

  function trouverAppId() {
    const textes = [];
    for (const s of document.querySelectorAll('script:not([src])')) textes.push(s.textContent || '');
    for (const motif of MOTIFS_APP_ID) {
      for (const t of textes) {
        const m = t.match(motif);
        if (m) return m[1];
      }
    }
    return '';
  }

  async function preparer() {
    if (!lireCookie('csrftoken')) throw new Arret('non_connecte', 'jeton anti-CSRF (cookie csrftoken) absent');
    const appId = trouverAppId();
    if (!appId) throw new Arret('introuvable', 'identifiant d\'application (x-ig-app-id) introuvable dans la page');
    journal('Identifiant d\'application lu dans la page : ' + appId + '.');
    return { appId };
  }

  function entetes(contexte) {
    return {
      'x-csrftoken': lireCookie('csrftoken'),
      'x-ig-app-id': contexte.appId,
      'x-requested-with': 'XMLHttpRequest',
    };
  }

  async function lireJson(reponse) {
    try {
      return await reponse.clone().json();
    } catch (e) {
      return null;
    }
  }

  // Lève un arrêt si la réponse signale une limite ou une session absente.
  function examiner(r, j, etape) {
    const message = String((j && j.message) || '').toLowerCase();
    const resume = etape + ' : HTTP ' + r.status + (message ? ', « ' + message.slice(0, 120) + ' »' : '');
    if (r.redirected && /\/accounts\/login/.test(r.url)) {
      throw new Arret('non_connecte', resume + ', renvoi vers la page de connexion');
    }
    if (r.status === 429 || (j && j.spam === true) || MOTIF_LIMITE.test(message)) throw new Arret('limite', resume);
    if (r.status === 401 || r.status === 403 || (j && j.require_login === true) || /login_required/.test(message)) {
      throw new Arret('non_connecte', resume);
    }
    return resume;
  }

  async function bloquer(contexte, h) {
    let r;
    try {
      r = await fetch(ORIGINE + '/api/v1/users/web_profile_info/?username=' + encodeURIComponent(h), {
        credentials: 'include',
        headers: entetes(contexte),
      });
    } catch (e) {
      throw new Arret('erreur', 'recherche du compte impossible (' + e.message + ')');
    }
    let j = await lireJson(r);
    const resume = examiner(r, j, 'recherche de @' + h);
    if (r.status === 404) {
      journal('@' + h + ' : compte introuvable, sauté.');
      return 'i';
    }
    if (!r.ok) throw new Arret('erreur', resume);
    const u = j && j.data && j.data.user;
    if (!u) {
      journal('@' + h + ' : compte introuvable, sauté.');
      return 'i';
    }
    // Le profil renvoyé doit avoir exactement l'identifiant demandé (les
    // identifiants Instagram ne tiennent pas compte de la casse) ; sinon,
    // rien n'est bloqué et le script s'arrête.
    const nom = typeof u.username === 'string' ? u.username : '';
    if (nom.toLowerCase() !== String(h).toLowerCase()) {
      throw new Arret('mauvais_compte', 'Instagram a renvoyé le profil ' + (nom ? '@' + nom : 'sans identifiant')
        + ' pour @' + h + ' ; aucun blocage n\'a été envoyé.');
    }
    const id = String(u.id || '');
    if (!/^\d{1,30}$/.test(id)) {
      throw new Arret('introuvable', 'identifiant numérique de @' + h + ' absent de la réponse');
    }
    if (u.blocked_by_viewer === true) {
      journal('@' + h + ' : était bloqué avant ce lancement, rien à faire.');
      return 'd';
    }
    try {
      r = await fetch(ORIGINE + '/api/v1/web/friendships/' + id + '/block/', {
        method: 'POST',
        credentials: 'include',
        headers: Object.assign(entetes(contexte), { 'content-type': 'application/x-www-form-urlencoded' }),
        body: '',
      });
    } catch (e) {
      throw new Arret('erreur', 'requête de blocage impossible (' + e.message + ')');
    }
    j = await lireJson(r);
    const resume2 = examiner(r, j, 'blocage de @' + h);
    if (r.ok && j && j.status === 'ok') {
      journal('@' + h + ' : bloqué.');
      return 'b';
    }
    if (r.status === 404) throw new Arret('introuvable', 'requête de blocage introuvable (' + resume2 + ')');
    throw new Arret('erreur', 'réponse inattendue (' + resume2 + ')');
  }

  // =====================================================================
  // Déroulement : un compte à la fois, par les requêtes de la page. La
  // liste, reçue du site au lancement, reste en mémoire le temps du
  // lancement : elle n'est enregistrée nulle part.
  // =====================================================================

  function prochainCompte(comptes, etat, vus) {
    for (const h of comptes) {
      if (!etat.faits[cle(h)] && !vus.has(cle(h))) return h;
    }
    return null;
  }

  // Lancé par ton clic (« Lancer » ou « Recevoir la liste et lancer ») : la
  // fenêtre du site est ouverte avant la première attente.
  async function executer() {
    if (window[DRAPEAU]) {
      avertir(MESSAGES.deja_en_cours);
      return;
    }
    arretDemande = false;
    const panneau = creerPanneau();
    if (location.origin !== ORIGINE) {
      panneau.fin('mauvais_site', null, 'page actuelle : ' + location.origin);
      return;
    }
    if (!stockageDisponible()) {
      panneau.fin('stockage', null, '');
      return;
    }
    let etat = lireEtat();
    if (lancementActifAilleurs(etat)) {
      panneau.fin('deja_en_cours', null, '');
      return;
    }
    // Pendant ton clic, avant toute attente : sans clic, le navigateur
    // bloque la fenêtre du site.
    const pont = ouvrirPont();
    window[DRAPEAU] = true;
    if (!DRY) {
      etat.lancement = { actif: true, onglet: ONGLET, battement: Date.now(), debut: Date.now() };
      ecrireEtat(etat);
    }

    let comptes = null; // liste reçue du site, en mémoire seulement
    const bilan = { bloques: 0, deja: 0, introuvables: 0 };
    const vus = new Set(); // comptes simulés en mode DRY
    const serie = []; // comptes introuvables de suite
    let n = 0;
    let code = 'termine';
    let detail = '';
    let affiche = '';
    const majProgression = () => {
      if (comptes) panneau.progression(n, comptes.length - compterRestants(lireEtat(), comptes), comptes.length);
    };

    try {
      const recue = await recevoirListe(pont, panneau);
      comptes = recue.comptes;
      panneau.liste(comptes.length);
      // Liste beaucoup plus longue que la dernière acceptée : ta confirmation d'abord.
      await accepterListe(recue, panneau);
      journal((DRY ? 'Essai à blanc (DRY) : rien ne sera envoyé. ' : '')
        + 'Lancement : ' + compterRestants(lireEtat(), comptes) + ' comptes restants sur ' + comptes.length
        + ', plafond ' + PLAFOND + ', pause fixe ' + SECONDES_PAUSE + ' s.');
      panneau.statut('Vérification de la session…');
      majProgression();
      const contexte = await preparer();
      for (;;) {
        if (arretDemande) throw new Arret('utilisateur');
        etat = lireEtat();
        const suivant = prochainCompte(comptes, etat, vus);
        if (!suivant) break;
        if (n >= PLAFOND) throw new Arret('plafond');
        if (n > 0) {
          // Pause fixe entre deux comptes, pour rester sous les limites de débit.
          panneau.statut('Pause de ' + SECONDES_PAUSE + ' s (limite de débit de ' + LIBELLE + ').');
          await pause(PAUSE_MS);
          if (arretDemande) throw new Arret('utilisateur');
        }
        panneau.courant(suivant);
        panneau.statut(DRY ? 'Essai à blanc…' : 'Blocage en cours…');
        let resultat;
        if (DRY) {
          journal('[DRY] Bloquerait @' + suivant + ' (rien n\'est envoyé).');
          vus.add(cle(suivant));
          resultat = 'b';
        } else {
          resultat = await bloquer(contexte, suivant);
        }
        n += 1;
        if (resultat === 'b') bilan.bloques += 1;
        if (resultat === 'd') bilan.deja += 1;
        if (resultat === 'i') {
          bilan.introuvables += 1;
          serie.push(cle(suivant));
        } else {
          serie.length = 0;
        }
        if (!DRY) {
          etat = lireEtat();
          etat.faits[cle(suivant)] = resultat;
          ecrireEtat(etat);
        }
        if (INTROUVABLES_DE_SUITE_MAX > 0 && serie.length >= INTROUVABLES_DE_SUITE_MAX) {
          // Plusieurs comptes de suite introuvables : la requête ne répond
          // sans doute plus comme prévu. Ces comptes ne sont pas notés.
          etat = lireEtat();
          for (const k of serie) delete etat.faits[k];
          ecrireEtat(etat);
          bilan.introuvables -= serie.length;
          throw new Arret('introuvable', serie.length + ' comptes de suite introuvables (@' + serie.join(', @')
            + ') : la requête de recherche ne répond peut-être plus comme prévu. Ces comptes ne sont pas notés comme traités.');
        }
        majProgression();
      }
    } catch (e) {
      if (e instanceof Arret) {
        code = e.code;
        detail = e.detail;
        affiche = e.affiche;
      } else {
        code = 'erreur';
        detail = String((e && e.message) || e);
      }
    }
    etat = lireEtat();
    if (etat.lancement && etat.lancement.onglet === ONGLET) {
      etat.lancement = null;
      ecrireEtat(etat);
    }
    window[DRAPEAU] = false;
    majProgression();
    // Sans liste reçue, pas de bilan : rien n'a été tenté.
    panneau.fin(code, comptes ? bilan : null, detail, comptes ? compterRestants(etat, comptes) : undefined, affiche);
  }

  // =====================================================================
  // Encadré de lancement : rien ne se passe tant que tu ne cliques pas sur
  // son bouton (« Lancer », ou « Recevoir la liste et lancer » pour le
  // script console). Ce clic ouvre la petite fenêtre du site qui transmet
  // la liste : sans clic, le navigateur la bloquerait.
  // forcer : réafficher l'encadré (commande du menu, script collé de
  // nouveau), en retirant celui d'un lancement terminé.
  // =====================================================================

  function afficherLanceur(lancer, forcer) {
    if (forcer) {
      const ancien = document.getElementById('horsfil-lanceur');
      if (ancien) ancien.remove();
      const panneau = document.getElementById('horsfil-panneau');
      if (panneau && panneau.dataset.etat === 'fini') panneau.remove();
    }
    if (document.getElementById('horsfil-lanceur') || document.getElementById('horsfil-panneau')) return;
    const boite = noeud('div', Object.assign({}, STYLE_BOITE, { left: '16px', width: '300px', padding: '10px 12px' }));
    boite.id = 'horsfil-lanceur';
    boite.setAttribute('aria-label', 'Hors fil — ' + LIBELLE);
    const masquer = bouton('Masquer', 'masquer', false);
    masquer.addEventListener('click', () => boite.remove());
    // Stockage du gestionnaire absent : pas de bouton de lancement.
    if (!stockageDisponible()) {
      boite.dataset.refus = 'stockage';
      boite.appendChild(noeud('p', { margin: '0' }, 'Hors fil — ' + LIBELLE + ' : ' + MESSAGES.stockage));
      boite.appendChild(masquer);
      (document.body || document.documentElement).appendChild(boite);
      avertir(MESSAGES.stockage);
      return;
    }
    const etat = lireEtat();
    const ailleurs = lancementActifAilleurs(etat);
    const texte = ailleurs
      ? 'Hors fil — ' + LIBELLE + ' : un lancement est en cours dans un autre onglet.'
      : 'Hors fil — ' + LIBELLE + ' : bloquer sur ton compte les comptes de la liste'
        + (DRY ? ' (essai à blanc, DRY)' : '') + '.';
    boite.appendChild(noeud('p', { margin: '0' }, texte));
    if (!ailleurs) {
      // Le script ne connaît pas encore la liste : il ne montre que ce qu'il
      // a déjà traité dans ce navigateur.
      const traites = Object.keys(etat.faits).length;
      boite.appendChild(noeud('p', { margin: '4px 0 0', color: COULEURS.encre2, fontSize: '13px' },
        'Au clic, une petite fenêtre du site Hors fil s\'ouvre et transmet la liste à jour au script.'
        + (traites > 0 ? ' Déjà traités dans ce navigateur : ' + traites + '.' : '')));
      const b = bouton(SORTE === 'console' ? 'Recevoir la liste et lancer' : 'Lancer', 'lancer', true);
      b.addEventListener('click', () => {
        boite.remove();
        lancer();
      });
      boite.appendChild(b);
    }
    boite.appendChild(masquer);
    (document.body || document.documentElement).appendChild(boite);
  }

  // =====================================================================
  // Lancement : le script console affiche son encadré. Rien ne se passe
  // tant que tu ne cliques pas sur « Recevoir la liste et lancer » : sans
  // clic, le navigateur bloquerait la petite fenêtre du site.
  // =====================================================================
  journal('Version ' + VERSION + ' du ' + GENERE_LE + '. La liste est demandée au site Hors fil à chaque lancement.');
  if (location.origin !== ORIGINE) creerPanneau().fin('mauvais_site', null, 'page actuelle : ' + location.origin);
  else if (window[DRAPEAU]) avertir(MESSAGES.deja_en_cours);
  else afficherLanceur(executer, true);
})();

Userscript

Installer le userscript Instagram. Ton gestionnaire de scripts affiche le code avant de l’installer.

Empreintes SHA-256

  • instagram.console.js : 9807e15e15978a927c405782bffc0fa9c3ebd2fd7cd9974551e3170a466b6e52
  • instagram.user.js : 044642c5b1462576b13798c4ad9226359deebb2f5ff85f66e6b71828fe3fee80

Facebook

expérimental

Ouvre un à un les profils de la liste dans ton onglet ; si l'adresse est exactement celle du profil de la liste, il ouvre le menu du profil, choisit « Bloquer » et confirme, comme tu le ferais à la main. Il ne note un compte comme bloqué que lorsque la page affiche le blocage (« Débloquer » ou message de confirmation). La liste n'est pas écrite dans le script : au lancement, il ouvre une petite fenêtre du site Hors fil, qui demande au site la liste à jour et la lui transmet. Il avance avec une pause fixe de 3,5 secondes entre deux comptes et au plus 40 comptes par lancement ; un nouveau lancement reprend là où le précédent s'est arrêté.

Où le lancer
Sur www.facebook.com, avec ton compte ouvert : installe facebook.user.js dans un gestionnaire de scripts (Violentmonkey, Tampermonkey), puis clique sur « Lancer » dans l'encadré Hors fil. Une petite fenêtre du site Hors fil s'ouvre alors et se referme seule ; si ton navigateur la bloque, autorise les fenêtres pop-up pour www.facebook.com. Laisse l'onglet ouvert pendant le lancement.
Méthode
interface
Comptes
691 dans la liste, mis à jour en direct
Par lancement
40 comptes au plus, une pause de 3,5 secondes entre deux
Version
2.0.0, vérifiée le 30 octobre 2026 sur page simulée

Ce qu’il ne fait pas

  • N'envoie lui-même aucune requête ni aucune donnée et ne charge aucun code distant : il ouvre des pages de Facebook et clique dans l'interface. Pour recevoir la liste, il ouvre seulement une page du site Hors fil dans une petite fenêtre, et c'est cette page qui demande la liste au site.
  • N'accepte la liste que de la fenêtre du site qu'il vient d'ouvrir, et la refuse en entier si un seul identifiant n'a pas la forme d'un identifiant Facebook ; il n'exécute rien de ce qu'il reçoit.
  • Ne bloque rien, si la liste a beaucoup grossi depuis ton dernier lancement (plus de 10 % et plus de 200 comptes de plus), avant que tu confirmes : c'est une protection si le site Hors fil était compromis.
  • Ne garde la liste que le temps du lancement, dans le stockage de ton gestionnaire de scripts : il l'efface à la fin du lancement, à l'arrêt, ou au plus tard 24 heures après l'avoir reçue ; chaque lancement la redemande au site.
  • Ne clique jamais, quelle que soit l'étape, sur un élément dont le texte ou le nom contient « Signaler », « Envoyer », « Supprimer », « Retirer », « Masquer », « Ne plus suivre », « Débloquer » ou leurs équivalents anglais ; s'il en trouve un à la place du bouton attendu, il s'arrête sans cliquer. Il ne remplit et n'envoie aucun formulaire.
  • Ne clique sur rien si la page ouverte n'est pas exactement celle du compte de la liste (redirection, autre adresse) : ce compte est noté « non trouvé ».
  • Ne note pas comme bloqué un compte dont la page n'affiche pas le blocage : il le note « non vérifié », à contrôler à la main, et s'arrête au deuxième cas de suite.
  • Ne garde sa progression (la liste des comptes déjà traités) que dans le stockage de ton gestionnaire de scripts, hors de portée des pages de Facebook ; dans la page, il ne garde qu'un identifiant d'onglet aléatoire, sans aucune donnée (sessionStorage), pour ne pas mener deux lancements à la fois.
  • Ne suit, ne signale et ne contacte aucun compte ; le lien « Suivre @BlockFarRight » affiché à la fin n'est qu'un lien.
  • Ne contourne aucune limite : il s'arrête dès que Facebook signale une limite de débit.
  • Ne se lance pas si cette copie est trop ancienne pour la liste du site : il te demande alors de télécharger la nouvelle version sur https://onbloquelextremedroite.org/outils/.
  • N'a pas encore été essayé sur le vrai site Facebook, seulement sur une page simulée ; si Facebook a changé son site, il s'arrête avec un message.

Userscript

Installer le userscript Facebook. Ton gestionnaire de scripts affiche le code avant de l’installer.

Empreintes SHA-256

  • facebook.user.js : 989993c238b00025f7053e7e5074fdcd10bfec7a3977b038ada0b641ef53792a

TikTok

expérimental

Ouvre un à un les profils de la liste dans ton onglet ; si l'adresse est exactement celle du profil de la liste, il ouvre le menu du profil, choisit « Bloquer » et confirme, comme tu le ferais à la main. Il ne note un compte comme bloqué que lorsque la page affiche le blocage (« Débloquer » ou message de confirmation). La liste n'est pas écrite dans le script : au lancement, il ouvre une petite fenêtre du site Hors fil, qui demande au site la liste à jour et la lui transmet. Il avance avec une pause fixe de 3,5 secondes entre deux comptes et au plus 40 comptes par lancement ; un nouveau lancement reprend là où le précédent s'est arrêté.

Où le lancer
Sur www.tiktok.com, avec ton compte ouvert : installe tiktok.user.js dans un gestionnaire de scripts (Violentmonkey, Tampermonkey), puis clique sur « Lancer » dans l'encadré Hors fil. Une petite fenêtre du site Hors fil s'ouvre alors et se referme seule ; si ton navigateur la bloque, autorise les fenêtres pop-up pour www.tiktok.com. Laisse l'onglet ouvert pendant le lancement.
Méthode
interface
Comptes
351 dans la liste, mis à jour en direct
Par lancement
40 comptes au plus, une pause de 3,5 secondes entre deux
Version
2.0.0, vérifiée le 30 octobre 2026 sur page simulée

Ce qu’il ne fait pas

  • N'envoie lui-même aucune requête ni aucune donnée et ne charge aucun code distant : il ouvre des pages de TikTok et clique dans l'interface. Pour recevoir la liste, il ouvre seulement une page du site Hors fil dans une petite fenêtre, et c'est cette page qui demande la liste au site.
  • N'accepte la liste que de la fenêtre du site qu'il vient d'ouvrir, et la refuse en entier si un seul identifiant n'a pas la forme d'un identifiant TikTok ; il n'exécute rien de ce qu'il reçoit.
  • Ne bloque rien, si la liste a beaucoup grossi depuis ton dernier lancement (plus de 10 % et plus de 200 comptes de plus), avant que tu confirmes : c'est une protection si le site Hors fil était compromis.
  • Ne garde la liste que le temps du lancement, dans le stockage de ton gestionnaire de scripts : il l'efface à la fin du lancement, à l'arrêt, ou au plus tard 24 heures après l'avoir reçue ; chaque lancement la redemande au site.
  • Ne clique jamais, quelle que soit l'étape, sur un élément dont le texte ou le nom contient « Signaler », « Envoyer », « Supprimer », « Retirer », « Masquer », « Ne plus suivre », « Débloquer » ou leurs équivalents anglais ; s'il en trouve un à la place du bouton attendu, il s'arrête sans cliquer. Il ne remplit et n'envoie aucun formulaire.
  • Ne clique sur rien si la page ouverte n'est pas exactement celle du compte de la liste (redirection, autre adresse) : ce compte est noté « non trouvé ».
  • Ne note pas comme bloqué un compte dont la page n'affiche pas le blocage : il le note « non vérifié », à contrôler à la main, et s'arrête au deuxième cas de suite.
  • Ne garde sa progression (la liste des comptes déjà traités) que dans le stockage de ton gestionnaire de scripts, hors de portée des pages de TikTok ; dans la page, il ne garde qu'un identifiant d'onglet aléatoire, sans aucune donnée (sessionStorage), pour ne pas mener deux lancements à la fois.
  • Ne suit, ne signale et ne contacte aucun compte ; le lien « Suivre @BlockFarRight » affiché à la fin n'est qu'un lien.
  • Ne contourne aucune limite : il s'arrête dès que TikTok signale une limite de débit.
  • Ne se lance pas si cette copie est trop ancienne pour la liste du site : il te demande alors de télécharger la nouvelle version sur https://onbloquelextremedroite.org/outils/.
  • N'a pas encore été essayé sur le vrai site TikTok, seulement sur une page simulée ; si TikTok a changé son site, il s'arrête avec un message.

Userscript

Installer le userscript TikTok. Ton gestionnaire de scripts affiche le code avant de l’installer.

Empreintes SHA-256

  • tiktok.user.js : b3174f325ff160e180f6edadc06f8cb7bbdffc11665b99f19348e769046bee0a