Created
May 10, 2023 10:36
-
-
Save martinratinaud/8775130a2a81d6f5008d9cf2c7206ad3 to your computer and use it in GitHub Desktop.
Add nb followers and following directly in Twitter feed
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 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