ENFRKOantony langlois
projets

Instagram Unfollower Scanner

visiterextension chromeChrome ExtensionJavaScript

Extension Chrome qui trouve qui ne vous suit pas en retour · aucun backend, aucun transfert d'identifiants · s'appuie sur la propre session Instagram de l'utilisateur via un content script injecté

Architecture

vanilla JS
Frontend · popup d'action
Popup UI
popup · popup.js
Restaure le dernier scan à l'ouverture, affiche les listes de catégories, flux de confirmation avant désabonnement
popup thread
Analyse · scoring d'influenceurs
Profile categorizer
popup.js · categorizeUsers
Score les comptes (vérifié, ratio d'abonnés, business) pour séparer les influenceurs des utilisateurs classiques
runs on instagram.com
Cœur · client API injecté
Content script
content · content.js
Injecté dans la page Instagram pour que chaque fetch porte les propres cookies de session de l'utilisateur
pagination par curseurkill switch 429rythme de 300 msCSRF depuis les cookiesPOST follow / unfollowhydratation de profils
5 fallbacks
Identité · détection de connexion
Session detector
content.js · isUserLoggedIn
Trouve l'état de connexion et l'identifiant utilisateur sur 40+ langues d'interface via alt text, heuristiques de nav, cookies
per-account keys
Stockage · local
chrome.storage.local
instagramAnalysisResults_<id>
Dernier scan avec timestamp par compte, cache de profils de 7 jours, liste des récemment désabonnés
api/v1
Externe · API privée
Instagram web API
friendships · web_profile_info
Endpoints non documentés, authentifiés par les cookies de session plus le header d'app id web

Vous ouvrez le popup sur n'importe quel onglet instagram.com. Le Popup UI restaure d'abord le scan précédent depuis chrome.storage.local et vérifie le cooldown de 24 h. Sur Analyze, il envoie un message au Content script, qui demande au Session detector l'identifiant de l'utilisateur connecté (cookies d'abord, requête de profil en repli). Le content script parcourt ensuite /friendships/&lt;id&gt;/followers (25 par page) et /following (200 par page) contre l'Instagram web API avec un rythme de 300 ms entre les requêtes. De retour dans le popup, le diff produit l'ensemble de ceux qui ne suivent pas en retour, le Profile categorizer hydrate chaque compte par lots de 5 pour séparer les influenceurs des utilisateurs classiques, et les résultats plus les caches sont réécrits dans le storage.

Déploiement : Extension → Chrome · Manifest V3, chargée non empaquetée ou via le Web Store

Résultats

  • 0 · serveurs · tout tourne dans le navigateur de l'utilisateur
  • 40+ · langues d'interface auxquelles la détection de connexion survit
  • 1er 429 · déclenche un kill switch, le scan se dégrade au lieu d'échouer
  • 24 h · cooldown de scan par compte, résultats restaurés entre-temps

Compétences démontrées : rétro-ingénierie d'API privée · pagination consciente des limites de débit · invalidation de cache (TTL de 7 jours) · message passing Chrome · gestion des tokens CSRF

Problèmes et solutions

Une contrainte gouverne tout : Instagram n'a pas d'API publique pour les listes d'abonnés, donc l'extension doit réutiliser la propre session de l'utilisateur sans jamais détenir d'identifiants ni déclencher les limites de débit.

█ S'appuyer sur la session de l'utilisateur, pas sur un backend

Problème : Il n'existe pas d'API officielle pour lister les abonnés. Un scraper côté serveur aurait besoin du mot de passe de l'utilisateur ou de cookies volés, plus de proxies pour survivre aux blocages. Les deux sont rédhibitoires pour la confiance et le coût.

Un content script injecté dans instagram.com appelle les mêmes endpoints privés que l'application web utilise. Les requêtes partent avec credentials: include, le propre user agent du navigateur, et le header d'app id web, donc aux yeux d'Instagram elles ressemblent à l'utilisateur en train de naviguer.

  • Backend de scraping rejeté : zéro gestion d'identifiants vaut mieux que le contrôle total du pipeline
  • Scraping DOM de la modale des abonnés rejeté : fragile face aux changements de balisage et bien plus lent que les endpoints JSON
  • Compromis accepté : les endpoints non documentés peuvent changer sans préavis, donc le code traite chaque réponse comme optionnelle

▒ Limites de débit : rythmer, détecter, dégrader

Problème : La pagination des abonnés plus une requête de profil par compte peut représenter des centaines de requêtes. Marteler des endpoints privés vaut un 429 et peut signaler le vrai compte de l'utilisateur.

Des pauses de 300 ms entre les pages, l'hydratation des profils par lots de 5, et un kill switch global : le premier 429 stoppe toute requête de profil ultérieure pour la session et le scan continue avec les données de base seulement.

  • Retry-with-backoff rejeté : réessayer contre un système anti-abus creuse le trou plus profond
  • Dégrader vaut mieux qu'échouer : les utilisateurs obtiennent toujours la liste complète de ceux qui ne suivent pas en retour, juste sans scores d'influenceur
  • Le kill switch est un drapeau à sens unique, délibérément conservateur pour protéger le compte de l'utilisateur

▚ Cooldown de scan de 24 heures

Problème : Le scan complet est l'opération la plus lourde. Les utilisateurs cliquant sur Analyze de façon répétée multiplient le risque de blocage pour zéro nouvelle information.

Chaque scan est horodaté par compte dans chrome.storage.local. Dans les 24 h, le bouton est désactivé avec un compte à rebours en direct et les résultats sauvegardés sont affichés à la place.

  • Scans illimités rejetés : protéger le compte de l'utilisateur prime sur la fraîcheur
  • Compromis accepté : les données peuvent être vieilles d'un jour au maximum
  • Les actions follow / unfollow mettent à jour les résultats sauvegardés en place, donc la vue en cache reste correcte entre les scans

▞ Détection de connexion en 40+ langues

Problème : La première version détectait la connexion par l'alt text anglais 'profile picture'. Toute interface Instagram non anglaise faisait croire à l'extension que l'utilisateur était déconnecté.

Cinq replis en couches : une table d'alt text localisée couvrant 40+ langues, une heuristique géométrique (petite image carrée dans la nav), les propres objets de données d'Instagram, le lien de déconnexion, et les cookies de session.

  • Un unique sélecteur 'fiable' rejeté : les noms de classe obscurcis d'Instagram changent trop souvent pour s'y fier
  • Les heuristiques peuvent faire des faux positifs individuellement, donc cinq signaux indépendants se recoupent
  • Le contrôle des cookies est le dernier recours car les cookies peuvent survivre à une vraie session

▓ Mettre en cache agressivement, indexer par compte

Problème : L'hydratation des profils coûte une requête par utilisateur analysé. Les rescans et les changements de compte re-paieraient ce coût et re-risqueraient les limites de débit.

Les résultats d'analyse par nom d'utilisateur vivent dans un cache de 7 jours ; les résultats de scan sont indexés par identifiant de compte, donc les personnes gérant plusieurs comptes conservent des historiques distincts qui survivent au changement.

  • Simplicité sans cache rejetée : les requêtes répétées sont la ressource la plus rare ici
  • 7 jours équilibrent l'obsolescence et le budget de requêtes, les nombres d'abonnés bougent lentement
  • Quand le kill switch 429 se déclenche, les profils en cache remplissent quand même les scores gratuitement

▤ Se désabonner sans perdre l'historique

Problème : Se désabonner de quelqu'un pendant la revue de la liste mute les données en dessous : l'utilisateur disparaît, et un rescan pour le refléter brûlerait le scan quotidien.

Les comptes désabonnés passent dans une liste des récemment désabonnés persistée au lieu de disparaître. Se réabonner les restaure dans la liste normale, et chaque changement édite les résultats sauvegardés en place.

  • Rescan-après-action rejeté : un désabonnement ne devrait pas coûter un scan complet
  • Suppression silencieuse rejetée : les utilisateurs veulent un chemin d'annulation et une trace de qui ils ont retiré
  • Les transitions d'état vivent dans le storage, donc fermer le popup en plein nettoyage ne perd rien