GitHub Follower Tracker

Track GitHub followers&following

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         GitHub Follower Tracker
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Track GitHub followers&following
// @author       maanimis
// @match        https://github.com/*?tab=following
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_listValues
// @run-at       document-end
// @license MIT
// @icon https://github.githubassets.com/pinned-octocat.svg
// ==/UserScript==

(function () {
  "use strict";

  const isDebugMode = false;

  const FOLLOWERS_PER_PAGE = 100;
  const API_BASE_URL = "https://api.github.com/users";

  let currentFollowers = [];
  let currentFollowing = [];
  let hasMorePages = false;
  let notFollowingBackList = [];
  let followsYouList = [];
  let notFollowingYouList = [];
  let isDataLoaded = false;

  function log(...args) {
    if (isDebugMode) {
      console.log(...args);
    }
  }

  function init() {
    const username = extractUsernameFromPage();
    if (!username) {
      log("[GitHub Tracker] Not on a user profile page");
      return;
    }

    log(`[GitHub Tracker] Tracking user: ${username}`);
    addLoadingLabels();
    processUserData(username);
  }

  function extractUsernameFromPage() {
    const href = document
      .querySelector('a[class="AppHeader-context-item"]')
      .getAttribute("href");
    return href.slice(1);
  }

  async function processUserData(username) {
    try {
      const followersUrl = `${API_BASE_URL}/${username}/followers?per_page=${FOLLOWERS_PER_PAGE}&page=`;
      const followingUrl = `${API_BASE_URL}/${username}/following?per_page=${FOLLOWERS_PER_PAGE}&page=`;

      log("[GitHub Tracker] Fetching followers...");
      currentFollowers = await fetchAllPages(followersUrl);
      log(`[GitHub Tracker] Total followers: ${currentFollowers.length}`);

      log("[GitHub Tracker] Fetching following...");
      currentFollowing = await fetchAllPages(followingUrl);
      log(`[GitHub Tracker] Total following: ${currentFollowing.length}`);

      notFollowingBackList = findNotFollowingBack();
      followsYouList = findFollowsYou();
      notFollowingYouList = findNotFollowingYou();

      logNotFollowingBack();

      const previousData = loadStoredData(username);

      if (previousData) {
        const changes = calculateChanges(previousData);
        logChanges(changes, previousData.timestamp);
      } else {
        log(`[GitHub Tracker] First time tracking ${username}`);
      }
      saveUserData(username);

      isDataLoaded = true;
      addFollowLabels();
    } catch (error) {
      log(`[GitHub Tracker] Error: ${error.message}`);
    }
  }

  async function fetchAllPages(baseUrl) {
    let page = 0;
    let allResults = [];

    do {
      hasMorePages = false;
      page++;

      const pageResults = await fetchPage(baseUrl, page);
      log(`[GitHub Tracker] Fetched page ${page}: ${pageResults.length} items`);

      allResults.push(...pageResults);

      if (pageResults.length === FOLLOWERS_PER_PAGE) {
        hasMorePages = true;
      }
    } while (hasMorePages);

    return allResults;
  }

  async function fetchPage(url, page = 1) {
    const response = await fetch(url + page);

    if (response.status === 404) {
      throw new Error("Username doesn't exist");
    }

    if (response.status === 403) {
      throw new Error("API rate limit exceeded");
    }

    return await response.json();
  }

  function saveUserData(username) {
    const dataToSave = {
      followers: currentFollowers,
      following: currentFollowing,
      timestamp: new Date().toISOString(),
    };

    GM_setValue(username, JSON.stringify(dataToSave));
    log(`[GitHub Tracker] Saved data for ${username}`);
  }

  function loadStoredData(username) {
    const storedData = GM_getValue(username);
    return storedData ? JSON.parse(storedData) : null;
  }

  function calculateChanges(previousData) {
    const removedFollowing = findRemovedUsers(
      previousData.following,
      currentFollowing
    );
    const addedFollowing = findAddedUsers(
      previousData.following,
      currentFollowing
    );
    const removedFollowers = findRemovedUsers(
      previousData.followers,
      currentFollowers
    );
    const addedFollowers = findAddedUsers(
      previousData.followers,
      currentFollowers
    );

    return {
      removedFollowing,
      addedFollowing,
      removedFollowers,
      addedFollowers,
    };
  }

  function findRemovedUsers(previousList, currentList) {
    return previousList.filter(
      (prevUser) => !currentList.some((currUser) => currUser.id === prevUser.id)
    );
  }

  function findAddedUsers(previousList, currentList) {
    return currentList.filter(
      (currUser) =>
        !previousList.some((prevUser) => prevUser.id === currUser.id)
    );
  }

  function findNotFollowingBack() {
    return currentFollowing.filter(
      (followingUser) =>
        !currentFollowers.some((follower) => follower.id === followingUser.id)
    );
  }

  function findFollowsYou() {
    return currentFollowers.filter((follower) =>
      currentFollowing.some((followingUser) => followingUser.id === follower.id)
    );
  }

  function findNotFollowingYou() {
    return currentFollowers.filter(
      (follower) =>
        !currentFollowing.some(
          (followingUser) => followingUser.id === follower.id
        )
    );
  }

  function logNotFollowingBack() {
    log(
      `\n[GitHub Tracker] 🔍 NOT FOLLOWING BACK (${notFollowingBackList.length} users):`
    );
    log("━".repeat(50));

    if (notFollowingBackList.length === 0) {
      log("   Everyone you follow is following you back! 🎉");
    } else {
      notFollowingBackList.forEach((user) => {
        log(`   • ${user.login} (${user.html_url})`);
      });
    }

    log("━".repeat(50));
  }

  function logChanges(changes, lastCheckDate) {
    const date = new Date(lastCheckDate).toLocaleString();
    log(`\n[GitHub Tracker] Changes since ${date}:`);
    log("━".repeat(50));

    log(
      `\n📊 FOLLOWING CHANGES (${
        changes.removedFollowing.length + changes.addedFollowing.length
      } total):`
    );
    logUserList("➖ Unfollowed", changes.removedFollowing);
    logUserList("➕ New Following", changes.addedFollowing);

    log(
      `\n📊 FOLLOWERS CHANGES (${
        changes.removedFollowers.length + changes.addedFollowers.length
      } total):`
    );
    logUserList("➖ Lost Followers", changes.removedFollowers);
    logUserList("➕ New Followers", changes.addedFollowers);

    log("\n" + "━".repeat(50));
  }

  function logUserList(label, userList) {
    if (userList.length === 0) {
      log(`   ${label}: None`);
      return;
    }

    log(`   ${label}: ${userList.length}`);
    userList.forEach((user) => {
      log(`      • ${user.login} (${user.html_url})`);
    });
  }

  function addLoadingLabels() {
    const usernameElements = document.querySelectorAll(
      'span[class="Link--secondary pl-1"]'
    );

    usernameElements.forEach((element) => {
      const existingLabel = element.nextElementSibling;
      if (
        existingLabel &&
        existingLabel.classList.contains("follow-status-label")
      ) {
        return;
      }

      const labelSpan = document.createElement("span");
      labelSpan.className = "follow-status-label loading-label";
      labelSpan.textContent = "loading...";
      labelSpan.style.cssText = `
        margin-left: 8px;
        padding: 3px 10px;
        background: linear-gradient(135deg, #8b949e 0%, #6e7781 100%);
        color: #ffffff;
        border-radius: 14px;
        font-size: 11px;
        font-weight: 600;
        letter-spacing: 0.3px;
        text-transform: uppercase;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
        display: inline-block;
        vertical-align: middle;
        opacity: 0.7;
        animation: pulse 1.5s ease-in-out infinite;
      `;

      element.parentNode.insertBefore(labelSpan, element.nextSibling);
    });

    const style = document.createElement("style");
    style.textContent = `
      @keyframes pulse {
        0%, 100% { opacity: 0.7; }
        50% { opacity: 0.4; }
      }
    `;
    document.head.appendChild(style);
  }

  function addFollowLabels() {
    const usernameElements = document.querySelectorAll(
      'span[class="Link--secondary pl-1"]'
    );

    usernameElements.forEach((element) => {
      const username = element.textContent.trim().toLowerCase();

      const existingLabel = element.nextElementSibling;
      if (
        existingLabel &&
        existingLabel.classList.contains("follow-status-label")
      ) {
        if (!isDataLoaded) {
          return;
        }
        if (existingLabel.classList.contains("loading-label")) {
          existingLabel.remove();
        } else {
          return;
        }
      }

      const isNotFollowingBack = notFollowingBackList.some(
        (user) => user.login.toLowerCase() === username
      );

      const isFollowsYou = followsYouList.some(
        (user) => user.login.toLowerCase() === username
      );

      const isNotFollowingYou = notFollowingYouList.some(
        (user) => user.login.toLowerCase() === username
      );

      let labelText = "";
      let backgroundColor = "";
      let textColor = "#ffffff";

      if (isNotFollowingBack) {
        labelText = "not following";
        backgroundColor = "#d73a49";
      } else if (isFollowsYou) {
        labelText = "follows you";
        backgroundColor = "#28a745";
      } else if (isNotFollowingYou) {
        labelText = "not following you";
        backgroundColor = "#6a737d";
      }

      if (labelText) {
        const labelSpan = document.createElement("span");
        labelSpan.className = "follow-status-label";
        labelSpan.textContent = labelText;
        labelSpan.style.cssText = `
          margin-left: 8px;
          padding: 3px 10px;
          background: linear-gradient(135deg, ${backgroundColor} 0%, ${adjustBrightness(
          backgroundColor,
          -15
        )} 100%);
          color: ${textColor};
          border-radius: 14px;
          font-size: 11px;
          font-weight: 600;
          letter-spacing: 0.3px;
          text-transform: uppercase;
          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
          display: inline-block;
          vertical-align: middle;
          transition: all 0.2s ease;
        `;

        labelSpan.addEventListener("mouseenter", function () {
          this.style.transform = "translateY(-1px)";
          this.style.boxShadow = "0 3px 6px rgba(0, 0, 0, 0.2)";
        });

        labelSpan.addEventListener("mouseleave", function () {
          this.style.transform = "translateY(0)";
          this.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.15)";
        });

        element.parentNode.insertBefore(labelSpan, element.nextSibling);
      }
    });
  }

  function adjustBrightness(color, percent) {
    const num = parseInt(color.replace("#", ""), 16);
    const amt = Math.round(2.55 * percent);
    const R = (num >> 16) + amt;
    const G = ((num >> 8) & 0x00ff) + amt;
    const B = (num & 0x0000ff) + amt;
    return (
      "#" +
      (
        0x1000000 +
        (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
        (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
        (B < 255 ? (B < 1 ? 0 : B) : 255)
      )
        .toString(16)
        .slice(1)
    );
  }

  const observer = new MutationObserver(() => {
    addFollowLabels();
  });

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      init();
      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    });
  } else {
    init();
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }
})();