GitHub Follower Tracker

Track GitHub followers&following

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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,
    });
  }
})();