Skip to content

Instantly share code, notes, and snippets.

@martinratinaud
Created May 10, 2023 10:36
Show Gist options
  • Save martinratinaud/8775130a2a81d6f5008d9cf2c7206ad3 to your computer and use it in GitHub Desktop.
Save martinratinaud/8775130a2a81d6f5008d9cf2c7206ad3 to your computer and use it in GitHub Desktop.
Add nb followers and following directly in Twitter feed
// ==UserScript==
// @name Twitter better list
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Add nb followers and following directly in feed
// @author Martin Ratinaud
// @match https://twitter.com/*
// @grant none
// ==/UserScript==
const css = `
.followers-count {
margin: 5px 0 5px 15px;
color: #cecece;
}
@media (prefers-color-scheme: light) {
.followers-count strong {
color: black;
}
}
@media (prefers-color-scheme: dark) {
.followers-count strong {
color: white;
}
}
`;
class Queue {
constructor() {
this.items = [];
this.isProcessing = false;
}
// Add an item to the queue
add(item) {
console.log('Add item');
this.items.push(item);
this.process();
}
// Process the items in the queue
async process() {
if (this.isProcessing || this.items.length === 0) {
return;
}
this.isProcessing = true;
while (this.items.length > 0) {
const currentItem = this.items.shift();
// Process the current item (assuming it returns a promise)
await currentItem();
}
this.isProcessing = false;
}
}
// Create a style element and add the CSS code
const styleElement = document.createElement('style');
styleElement.textContent = css;
// Append the style element to the document head
document.head.appendChild(styleElement);
const findClosestParentBySelector = (element, selector, initialElement = null) => {
return new Promise(async (resolve) => {
if (element === null) {
return resolve(null);
}
if (element.tagName === 'BODY') {
setTimeout(async () => {
const result = await findClosestParentBySelector(initialElement, selector);
resolve(result);
}, 5000);
} else {
// Store the first element if it's not already set
if (initialElement === null) {
initialElement = element;
}
if (element.matches(selector)) {
resolve(element);
} else {
const result = await findClosestParentBySelector(
element.parentNode,
selector,
initialElement
);
resolve(result);
}
}
});
};
const humanizeNumber = (number) => {
if (number >= 1000000) {
return (number / 1000000).toFixed(1) + 'M';
} else if (number >= 1000) {
return (number / 1000).toFixed(1) + 'k';
}
return number.toString();
};
const getUsersFromTimeline = (jsonResponse) => {
let userLegacies = [];
jsonResponse.data.home.home_timeline_urt.instructions.forEach((instruction) => {
instruction.entries?.forEach((entry) => {
const userLegacy =
entry?.content?.itemContent?.tweet_results?.result?.core?.user_results?.result?.legacy;
if (userLegacy) {
userLegacies.push(userLegacy);
} else if (entry?.content?.items) {
entry?.content?.items?.forEach((entryContentItem) => {
const userLegacy =
entryContentItem?.item?.itemContent?.tweet_results?.result?.core?.user_results?.result
?.legacy;
if (userLegacy) {
userLegacies.push(userLegacy);
}
});
} else {
// console.log(''); //eslint-disable-line
// console.log('╔════START══entry══════════════════════════════════════════════════'); //eslint-disable-line
// console.log(entry); //eslint-disable-line
// console.log('╚════END════entry══════════════════════════════════════════════════'); //eslint-disable-line
}
});
});
return userLegacies;
};
const queue = new Queue();
const getUsersFromSearch = (json) => {
if (!json?.globalObjects?.users) {
return [];
}
const users = Object.values(json.globalObjects.users);
return users;
};
const waitForElements = (selector) => {
return new Promise((resolve) => {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
resolve(elements);
}
const interval = setInterval(() => {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
clearInterval(interval);
resolve(elements);
}
}, 1000);
});
};
const waitForElement = (selector) => {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
}
const interval = setInterval(() => {
const element = document.querySelector(selector);
console.log(selector, element);
if (element) {
clearInterval(interval);
resolve(element);
}
}, 1000);
});
};
const processUser = async (user) => {
const userName = user.screen_name;
const userFollowersCount = user.followers_count;
const userFollowingsCount = user.friends_count;
const selector = `article a[href='/${userName}']:not([aria-hidden])`;
try {
const userDivs = await waitForElements(selector);
await Promise.all(
[...userDivs].map(async (userDiv) => {
const userDetailsDiv = await findClosestParentBySelector(
userDiv,
`[data-testid="User-Name"]`
);
if (!userDetailsDiv) {
return;
}
const parentDiv = userDetailsDiv.parentNode;
let followersCountParagraph = parentDiv.querySelector('span.followers-count');
if (!followersCountParagraph) {
followersCountParagraph = document.createElement('span');
followersCountParagraph.classList.add('followers-count');
parentDiv.appendChild(followersCountParagraph);
}
followersCountParagraph.innerHTML = `<strong>${humanizeNumber(
userFollowersCount
)}</strong> Followers - <strong>${humanizeNumber(userFollowingsCount)}</strong> Following`;
})
);
} catch (error) {
console.error(`Error processing user ${userName}:`, error);
}
};
const processFollowers = async (users) => {
try {
await Promise.all(users.map(processUser));
} catch (error) {
console.error('Error processing followers:', error);
}
};
const interval = setInterval(() => {
processFollowers(Object.values(allUsers));
}, 5000);
const removeDuplicatesById = (arr) => {
const uniqueItems = arr.reduce((accumulator, currentItem) => {
accumulator[currentItem.id] = currentItem;
return accumulator;
}, {});
return Object.values(uniqueItems);
};
let allUsers = {};
const addUsers = async (newUsers) => {
newUsers.forEach((newUser) => {
allUsers[newUser.screen_name] = {
screen_name: newUser.screen_name,
friends_count: newUser.friends_count,
followers_count: newUser.followers_count,
};
});
};
(function () {
'use strict';
(function (open) {
XMLHttpRequest.prototype.open = function (method, url) {
this.addEventListener(
'readystatechange',
async function () {
if (this.readyState === 4 && this.status === 200) {
if (
this.responseType !== 'arraybuffer' &&
this.responseText &&
this.responseText.includes('stackbli')
) {
console.log('URL:', url);
console.log('Response content:', JSON.parse(this.responseText));
// if (this.responseText.includes('1388024060827811841')) {
debugger;
}
try {
const json = JSON.parse(this.responseText);
if (url.includes('HomeTimeline')) {
const users = getUsersFromTimeline(json);
queue.add(async () => addUsers(users));
} else if (url.includes('/search/')) {
const users = getUsersFromSearch(json);
queue.add(async () => addUsers(users));
} else if (url.includes('/notifications/')) {
const users = getUsersFromSearch(json);
queue.add(async () => addUsers(users));
} else {
}
} catch (e) {}
}
},
false
);
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment