Auto Unfollow Instagram

Automatically unfollow users who do not follow you back on Instagram with UI control and progress bar.

// ==UserScript==
// @name         Auto Unfollow Instagram
// @namespace    https://kohardsi.my.id
// @version      3.1.0
// @description  Automatically unfollow users who do not follow you back on Instagram with UI control and progress bar.
// @author       Teja Sukmana
// @match        https://www.instagram.com/*
// @icon         https://kohardsi.my.id/wp-content/uploads/2024/12/logo-kohar.png
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @grant        GM_openInTab
// @license      MIT
// @run-at       document-idle
// @homepage     https://kohardsi.my.id
// @supportURL   https://kohardsi.my.id
// ==/UserScript==

(async () => {
  /* ========== HELPER ========== */
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const randomDelay = (base = 7000, variation = 3000) =>
    base + Math.floor(Math.random() * variation);
  const getCookie = name => {
    const v = `; ${document.cookie}`.split(`; ${name}=`);
    return v.length === 2 ? v.pop().split(';').shift() : null;
  };

  /* akun yang tidak akan di‑unfollow */
  const SKIP_USERS = ['kohardsi', 'instagram', 'meta', 'verified'];
  const SKIP_USERS_LOWER = SKIP_USERS.map(u => u.toLowerCase());

  /* ========== UI ========== */
  const ui = document.createElement('div');
  ui.innerHTML = `
  <style>
    #unfollowPanel h3{ text-align:center;font-size:20px;padding:0 0 10px;}
    #unfollowFloatingBtn{position:fixed;bottom:20px;right:20px;z-index:999999;background:#1e1e1e;color:#fff;border:none;border-radius:50%;width:50px;height:50px;font-size:24px;cursor:pointer;box-shadow:0 0 10px rgba(0,0,0,.3);}
    #unfollowPanel{position:fixed;bottom:80px;right:20px;background:#121212;color:#fff;border-radius:12px;padding:20px;max-width:300px;font-family:sans-serif;box-shadow:0 0 20px rgba(0,0,0,.4);z-index:999999;display:none;}
    #unfollowPanel input{width:100%;padding:6px;margin:5px 0 10px;background:#2c2c2c;color:#fff;border:1px solid #444;border-radius:6px;}
    #unfollowPanel button{width:100%;padding:8px;margin-top:10px;border:none;border-radius:6px;cursor:pointer;}
    #unfollowStartBtn{background:green;color:#fff;}
    #unfollowPauseBtn{background:#555;color:#fff;}
    #unfollowCloseBtn{background:crimson;color:#fff;}
    #unfollowList{margin-top:10px;max-height:100px;overflow-y:auto;font-size:12px;}
    #unfollowProgress{background:#444;border-radius:4px;height:10px;overflow:hidden;margin:10px 0;}
    #unfollowProgressBar{height:100%;background:limegreen;width:0%;transition:width .3s;}
    .rate-limit-warning{color:orange;font-size:12px;margin-top:10px;display:none;}
  </style>
  <button id="unfollowFloatingBtn">⚙️</button>
  <div id="unfollowPanel">
    <h3>Auto Unfollow</h3>
    <div class="rate-limit-warning" id="rateLimitWarning">⚠️ Jeda 15 menit karena batasan Instagram</div>
    <label>Limit Unfollow:</label><input type="number" id="unfollowLimit" value="10" min="1"/>
    <label>Jeda per akun (ms):</label><input type="number" id="unfollowDelay" value="7000" min="3000"/>
    <button id="unfollowStartBtn">Mulai</button>
    <button id="unfollowPauseBtn">Pause</button>
    <button id="unfollowCloseBtn">Tutup</button>
    <div id="unfollowProgress"><div id="unfollowProgressBar"></div></div>
    <div id="unfollowCount">Total di‑unfollow: 0</div>
    <div id="unfollowList"></div>
    <div style="margin-top:10px;font-size:10px;">© <a href="https://instagram.com/kohardsi" target="_blank" style="color:#999;">kohardsi</a></div>
  </div>`;
  document.body.appendChild(ui);

  /* ========== UI refs ========== */
  const btnToggle = document.getElementById('unfollowFloatingBtn');
  const panel = document.getElementById('unfollowPanel');
  const startBtn = document.getElementById('unfollowStartBtn');
  const pauseBtn = document.getElementById('unfollowPauseBtn');
  const closeBtn = document.getElementById('unfollowCloseBtn');
  const unfollowListDiv = document.getElementById('unfollowList');
  const unfollowCountEl = document.getElementById('unfollowCount');
  const progressBar = document.getElementById('unfollowProgressBar');
  const rateLimitWarning = document.getElementById('rateLimitWarning');

  let paused = false, totalToUnfollow = 0, unfollowedCount = 0;

  btnToggle.onclick = () => panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  closeBtn.onclick = () => panel.style.display = 'none';
  pauseBtn.onclick = () => {
    paused = !paused;
    pauseBtn.textContent = paused ? 'Lanjutkan' : 'Jeda';
    pauseBtn.style.background = paused ? 'orange' : '#555';
  };

  const updateProgress = () => {
    progressBar.style.width = totalToUnfollow ? (unfollowedCount / totalToUnfollow * 100) + '%' : '0%';
    unfollowCountEl.textContent = `Total di‑unfollow: ${unfollowedCount}/${totalToUnfollow}`;
  };
  const updateList = (u, fail=false) => {
    const d = document.createElement('div');
    d.textContent = `${fail ? '❌ Gagal' : '✅ Berhasil'}: ${u}`;
    d.style.color = fail ? 'orangered' : 'lightgreen';
    unfollowListDiv.appendChild(d);
    unfollowListDiv.scrollTop = unfollowListDiv.scrollHeight;
  };

  /* ---- Unfollow satu akun ---- */
  const unfollowUser = async (userId, username, attempt = 1) => {
    const csrftoken = getCookie('csrftoken');
    if (!csrftoken) { updateList(username, true); return; }

    try {
      const response = await fetch(`https://www.instagram.com/web/friendships/${userId}/unfollow/`, {
        method:'POST',
        headers:{
          'X-CSRFToken':csrftoken,'Accept':'*/*',
          'Content-Type':'application/x-www-form-urlencoded',
          'X-Requested-With':'XMLHttpRequest','Referer':location.href,
          'User-Agent':navigator.userAgent
        },credentials:'include'
      });

      /* === PATCH 1: deteksi HTTP 429 === */
      if (response.status === 429) throw new Error('rate limited');

      const data = await response.json();
      if (data.status === 'ok') {
        unfollowedCount++; updateList(username); updateProgress();
      } else throw new Error(data.message||'Unknown');
    } catch (e) {
      console.error(`Unfollow ${username} error:`, e.message);
      if (e.message.includes('rate limited') || attempt >= 3) {
        rateLimitWarning.style.display = 'block'; paused = true;
        pauseBtn.textContent = 'Lanjutkan'; pauseBtn.style.background = 'orange';
        setTimeout(()=>{ paused=false;rateLimitWarning.style.display='none';
          pauseBtn.textContent='Jeda';pauseBtn.style.background='#555'; },900000);
        updateList(username,true); return;
      }
      await sleep(2000); return unfollowUser(userId, username, attempt+1);
    }
    await sleep(randomDelay(+document.getElementById('unfollowDelay').value||7000));
  };

  /* ---- cek hubungan detail ---- */
/* =======================================================
   HYBRID FRIENDSHIP CHECKER
   ======================================================= */

/* — a. GraphQL web (prioritas) — */
const queryFriendHash = '3cabfbb5a7206eb0a60c5f93d3b1d6b'; // relationship hash
async function checkFriendshipGraphQL(id) {
  try {
    const vars = { user_id: id };
    const r = await fetch(
      `https://www.instagram.com/graphql/query/?query_hash=${queryFriendHash}&variables=${encodeURIComponent(JSON.stringify(vars))}`,
      {
        headers: {
          'X-CSRFToken': getCookie('csrftoken'),
          'Accept': '*/*',
          'X-Requested-With': 'XMLHttpRequest',
          'Referer': location.href,
          'User-Agent': navigator.userAgent
        },
        credentials: 'include'
      }
    );
    if (!r.ok) throw new Error(r.status);
    const d = await r.json();
    return {
      followed_by: d.data.user.is_followed_by_viewer,
      following  : d.data.user.follows_viewer
    };
  } catch (err) {
    console.warn('GraphQL friendship error', err);
    return null;
  }
}

/* — b. Mobile‑API fallback — */
async function checkFriendshipMobile(id) {
  try {
    const r = await fetch(
      `https://www.instagram.com/api/v1/friendships/show/${id}/`,
      {
        headers: {
          'X-CSRFToken': getCookie('csrftoken'),
          'X-IG-App-ID': '936619743392459',
          'X-IG-WWW-Claim': '0',
          'Accept': '*/*',
          'X-Requested-With': 'XMLHttpRequest',
          'Referer': location.href,
          'User-Agent': navigator.userAgent
        },
        credentials: 'include'
      }
    );
    if (!r.ok) throw new Error(r.status);
    const d = await r.json();
    return { followed_by: d.followed_by, following: d.following };
  } catch (err) {
    console.warn('Mobile friendship error', err);
    return null;
  }
}

/* — c. Wrapper yang memilih otomatis — */
let currentChecker = checkFriendshipGraphQL;     // mulai dengan GraphQL
async function safeCheckFriendship(id) {
  let rel = await currentChecker(id);
  if (rel === null && currentChecker === checkFriendshipGraphQL) {
    console.warn('⌛ GraphQL gagal – switch ke Mobile API');
    currentChecker = checkFriendshipMobile;      // ganti sekali untuk sesi ini
    rel = await currentChecker(id);
  }
  return rel;
}


  /* ---- Tombol MULAI ---- */
startBtn.onclick = async () => {
  paused = false;
  unfollowedCount = 0;
  unfollowListDiv.innerHTML = '';
  rateLimitWarning.style.display = 'none';
  updateProgress();
  unfollowCountEl.textContent = '🔄  Mengumpulkan data … harap tunggu';

  const limit = parseInt(document.getElementById("unfollowLimit").value) || 10;
    const ds_user_id = getCookie('ds_user_id');
    if(!ds_user_id){alert('❌ Login ulang! ds_user_id hilang');return;}

    const queryHash = 'c56ee0ae1f89cdbd1c89e2bc6b8f3d18';
    let hasNextPage = true, endCursor=null, toUnfollow=[];

    while(hasNextPage && toUnfollow.length<limit){
      const variables={id:ds_user_id,include_reel:true,fetch_mutual:false,first:50,after:endCursor};
      try{
        const r = await fetch(`https://www.instagram.com/graphql/query/?query_hash=${queryHash}&variables=${encodeURIComponent(JSON.stringify(variables))}`,{
          headers:{'X-CSRFToken':getCookie('csrftoken'),'Accept':'*/*','X-Requested-With':'XMLHttpRequest','Referer':location.href,'User-Agent':navigator.userAgent},
          credentials:'include'
        });
        const data=await r.json(), edges=data.data.user.edge_follow.edges;
        for(const edge of edges){
          const user=edge.node, uname=user.username.toLowerCase();
          if(SKIP_USERS_LOWER.includes(uname)||user.is_verified) continue;

          /* kasus 1: flag lengkap */
          if(user.followed_by_viewer===true && user.follows_viewer===false){
            toUnfollow.push({id:user.id,username:user.username});
          }
          /* kasus 2: flag null → cek API detail */
          else if(user.followed_by_viewer===true && user.follows_viewer==null){
            const fr = await safeCheckFriendship(user.id);
            if(fr && fr.following && !fr.followed_by){
              toUnfollow.push({id:user.id,username:user.username});
            } else if(!fr){
              updateList(user.username,true);        /* === PATCH 2 === */
            }
            await sleep(1000);
          }
          if(toUnfollow.length>=limit) break;
        }
        hasNextPage=data.data.user.edge_follow.page_info.has_next_page;
        endCursor=data.data.user.edge_follow.page_info.end_cursor;
        await sleep(2000);
      }catch(e){console.error('Get following error:',e);break;}
    }

    totalToUnfollow=Math.min(toUnfollow.length,limit); updateProgress();
    for(const u of toUnfollow.slice(0,limit)){
      while(paused) await sleep(1000);
      await unfollowUser(u.id,u.username);
    }
    alert(`✅ Selesai! Berhasil unfollow: ${unfollowedCount}/${totalToUnfollow} akun`);
  };
})();