// ==UserScript==
// @name twitter users export
// @namespace https://dun.mianbaoduo.com/@fun
// @version 0.1
// @description export twitter follower
// @author fun
// @match *://twitter.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant none
// @license GPL
// ==/UserScript==
(function () {
"use strict";
let authorizationCode = `Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA`;
function wait(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(";").shift();
}
function getUserMeta(user) {
if (user.__typename != "User") return null;
const {
screen_name,
entities,
created_at,
followers_count,
friends_count,
description,
location,
verified,
statuses_count,
profile_image_url_https,
following,
name,
} = user.legacy;
return {
avatar: profile_image_url_https,
userId: user.rest_id,
created_at,
description,
followers_count,
friends_count,
location,
verified,
name,
screen_name,
statuses_count,
following,
url:
entities.url && entities.url.urls.length
? entities.url.urls[0].expanded_url
: null,
};
}
class TwitterAPIHelper {
static async fetchInfo(id) {
const variables = encodeURIComponent(
`{"screen_name":"${id}","withSafetyModeUserFields":true,"withSuperFollowsUserFields":false}`
);
const req = await fetch(
"https://twitter.com/i/api/graphql/B-dCk4ph5BZ0UReWK590tw/UserByScreenName?variables=" +
variables,
{
headers: {
accept: "*/*",
"accept-language": "zh-CN,zh;q=0.9,en-IN;q=0.8,en;q=0.7,ar;q=0.6",
authorization: authorizationCode,
"content-type": "application/json",
"sec-ch-ua":
'"Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": getCookie("ct0"),
"x-twitter-active-user": "yes",
"x-twitter-auth-type": "OAuth2Session",
"x-twitter-client-language": "en",
},
referrer: "https://twitter.com/avinash_81",
referrerPolicy: "strict-origin-when-cross-origin",
body: null,
method: "GET",
mode: "cors",
credentials: "include",
}
);
const info = await req.json();
console.log(info);
if (!info.data.user) return null;
return info.data.user && getUserMeta(info.data.user.result);
}
static async getFollowing(id, count = 20) {
// const csfrToken = getCookie('ct0');
const args = {
userId: "" + id + "",
count: count,
withTweetQuoteCount: false,
includePromotedContent: false,
withSuperFollowsUserFields: true,
withUserResults: true,
withNftAvatar: false,
withBirdwatchPivots: false,
withReactionsMetadata: false,
withReactionsPerspective: false,
withSuperFollowsTweetFields: true,
};
const data = encodeURIComponent(JSON.stringify(args));
// const authorizationCode = this.authorization;
if (authorizationCode == null) {
throw new Error("please open twitter");
}
const req = await fetch(
"https://twitter.com/i/api/graphql/Fl6SSu1BCrwN6m-rLacKqg/Following?variables=" +
data,
{
headers: {
accept: "*/*",
"accept-language": "zh-CN,zh;q=0.9,en-IN;q=0.8,en;q=0.7,ar;q=0.6",
authorization: authorizationCode,
"content-type": "application/json",
"sec-ch-ua":
'"Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": getCookie("ct0"),
"x-twitter-active-user": "yes",
"x-twitter-auth-type": "OAuth2Session",
"x-twitter-client-language": "en",
},
referrer: "https://twitter.com/",
referrerPolicy: "strict-origin-when-cross-origin",
body: null,
method: "GET",
mode: "cors",
credentials: "include",
}
);
const followings = await req.json();
// console.log(followings);
const items =
followings.data.user.result.timeline.timeline.instructions.find(
(_) => _.type == "TimelineAddEntries"
);
// console.log(items);
const users = items.entries
.filter((_) => _.content.entryType == "TimelineTimelineItem")
.map((_) => {
const userItem = _.content.itemContent.user_results.result;
const parsedMeta = getUserMeta(userItem);
return {
...parsedMeta,
sortIndex: _.sortIndex,
};
});
return users;
}
static async getFollowers(id, count = 20, cursor = null) {
const args = {
userId: id,
count: 20,
cursor,
includePromotedContent: false,
withSuperFollowsUserFields: true,
withDownvotePerspective: false,
withReactionsMetadata: false,
withReactionsPerspective: false,
withSuperFollowsTweetFields: true,
};
const features = encodeURIComponent(
JSON.stringify({
dont_mention_me_view_api_enabled: true,
interactive_text_enabled: true,
responsive_web_uc_gql_enabled: false,
vibe_tweet_context_enabled: false,
responsive_web_edit_tweet_api_enabled: false,
standardized_nudges_misinfo: false,
responsive_web_enhance_cards_enabled: false,
})
);
const data = encodeURIComponent(JSON.stringify(args));
if (authorizationCode == null) {
throw new Error("please open twitter");
}
const req = await fetch(
"https://twitter.com/i/api/graphql/Fl6SSu1BCrwN6m-rLacKqg/Followers?variables=" +
data +
"&features=" +
features,
{
headers: {
accept: "*/*",
"accept-language": "zh-CN,zh;q=0.9,en-IN;q=0.8,en;q=0.7,ar;q=0.6",
authorization: authorizationCode,
"content-type": "application/json",
"sec-ch-ua":
'"Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": getCookie("ct0"),
"x-twitter-active-user": "yes",
"x-twitter-auth-type": "OAuth2Session",
"x-twitter-client-language": "en",
},
referrer: "https://twitter.com/",
referrerPolicy: "strict-origin-when-cross-origin",
body: null,
method: "GET",
mode: "cors",
credentials: "include",
}
);
const followings = await req.json();
// console.log(followings);
const items =
followings.data.user.result.timeline.timeline.instructions.find(
(_) => _.type == "TimelineAddEntries"
);
// console.log(items);
const cursors = items.entries
.filter((_) => _.content.entryType == "TimelineTimelineCursor")
.reduce((all, item) => {
all[item.content.cursorType] = item.content.value;
return all;
}, {});
const users = items.entries
.filter((_) => _.content.entryType == "TimelineTimelineItem")
.map((_) => {
const userItem = _.content.itemContent.user_results.result;
const parsedMeta = getUserMeta(userItem);
return {
...parsedMeta,
sortIndex: _.sortIndex,
};
});
return {
users,
cursors,
};
}
}
class LSStore {
async get(key) {
const value = localStorage.getItem(key);
if (value) {
return JSON.parse(value);
}
return null;
}
async set(key, value) {
return localStorage.setItem(key, JSON.stringify(value));
}
}
async function getUsers(userName) {
const user = await TwitterAPIHelper.fetchInfo(userName);
console.log("user", user);
let cursors = null;
let userPools = [];
for (let index = 0; index < Infinity; index++) {
const followers = await TwitterAPIHelper.getFollowers(
user.userId,
20,
cursors ? cursors.Bottom : null
);
printLog(`正在导出第 ${index+1} 页`);
cursors = followers.cursors;
console.log("followers", followers);
if (followers.users.length === 0) break;
if (userPools.length > 20) {
userPools.shift();
}
userPools.push(followers.users);
await new Promise((resolve, reject) => {
setTimeout(resolve, 8 * 1000);
});
}
download(
[]
.concat(
[
[
"userId",
"name",
"screen_name",
"created_at",
"followers_count",
"url",
"link"
].join(","),
],
userPools
.reduce((all, users) => {
users.forEach((_) => {
all.push(_);
});
return all;
}, [])
.map((_) => {
return [
_.userId,
`"${_.name}"`,
_.screen_name,
_.created_at,
// description,
_.followers_count,
// friends_count,
// location,
// verified,
// statuses_count,
// following,
_.url,
`https://twitter.com/${_.screen_name}`,
].join(",");
})
)
.join("\n"),
userName + ".csv",
"text/plain"
);
// return userPools;
}
let wrapper = document.createElement("div");
let backup = document.createElement("div");
backup.innerHTML = `<div><span id="bMSG"></span></div>
<div style="text-align: center;"></div> `;
function download(content, fileName, contentType) {
var a = document.createElement("a");
var file = new Blob([content], { type: contentType });
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
}
function printLog(msg) {
tip.innerText = msg;
}
backup.setAttribute(
"style",
"display:none; background: white; color: black; font-size: 13px; padding: 10px 10px 15px 10px;"
);
// backup.appendChild(tip);
const title = document.createElement("h2");
title.innerHTML = "粉丝导出";
title.setAttribute("style", "font-size: 15px;color: black;margin: 15px 0;");
wrapper.appendChild(title);
wrapper.appendChild(backup);
document.body.appendChild(wrapper);
wrapper.setAttribute(
"style",
`position: fixed;
border-radius: 3px;
background: white;
top: 80px;
right: 20px;
z-index: 100000;
padding:10px 15px;
text-align: center;
border: 1px solid #eee;
border-radius: 5px;
`
);
let started = false;
let allButtons = [];
function showAll() {
allButtons.forEach((btn) => {
btn.style.display = "block";
});
}
function hideAll() {
allButtons.forEach((btn) => {
btn.style.display = "none";
});
}
function createExport(name, type) {
let btn = document.createElement("button");
wrapper.appendChild(btn);
btn.innerHTML = name;
btn.setAttribute(
"style",
`border-radius: 0.166667rem; display: block; font-weight: bold; color: #444; margin:0 auto; padding: 5px 14px;font-size: 13px;text-align: center;border: 1px solid #cfcfcf;margin-top: 3px; cursor: pointer; margin-bottom: 7px;`
);
btn.addEventListener("click", async () => {
if (started) {
alert("started");
return;
}
started = true;
hideAll();
backup.style.display = "block";
// indicator.style.display = "inline-block";
const tabTile = document.querySelector('[property="al:android:url"]');
if (!tabTile) {
alert('user not found')
return;
}
const userName = document
.querySelector('[property="al:android:url"]')
.content.split("screen_name=")[1];
console.log("getUsers", userName);
await getUsers(userName);
started = false;
showAll();
// indicator.style.display = "none";
});
allButtons.push(btn);
}
let tip = document.getElementById("bMSG");
// let indicator = document.getElementById("bIndicator");
createExport("导出", "like");
})();