Last active
October 21, 2025 18:37
-
-
Save maanimis/0cdfe4aa60651bc0aacf90b033d04788 to your computer and use it in GitHub Desktop.
Track GitHub followers&following
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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, | |
| }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment