Skip to content

Instantly share code, notes, and snippets.

@evrimoztamur
Created January 25, 2017 11:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evrimoztamur/f0d192caafce2a0860f8f9e7fcbeefed to your computer and use it in GitHub Desktop.
Save evrimoztamur/f0d192caafce2a0860f8f9e7fcbeefed to your computer and use it in GitHub Desktop.
Creates two sets of "I'm not following them, but they are following me" and "I'm following them, but they aren't following me" when pasted into the browser console at the Instagram website.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// Pretend it's not safe.
(function(debug, usernamesOnly) {
// Hereafter 'followers' are the users that follow the viewer account, and
// 'following' are the users that are followed by the viewer account.
// The variable names are not named after the query names, but the interface
// names.
var headers = new Headers();
// 'window._sharedData.config.csrf_token' contains the CSRF token needed for
// the queries. CSRF token is mandatory, as the cookies alone do not enable
// the calls to be made.
headers.set("X-CSRFToken", window._sharedData.config.csrf_token);
// There's two other extension headers 'X-Instagram-Ajax' and
// 'X-Requested-With' for the wesbite, but they seems to be irrelevant.
function queryURLFormData(urlFormData, callback) {
fetch('/query/', {
method: "POST",
headers: headers,
credentials: 'include', // Include cookies and identification with the request
body: urlFormData
}).then(function(response) {
return response.json();
})
.then(function(data) {
callback(data);
});
}
// All of the following queries use '.first(10)' or '.after(end_cursor, 10)'
// as asking for more followers returns an arbitrary number of followers.
// There seems to be some rate limiting or database whizzery going on in the
// background, as even asking for 10 can return, say, 5 entries. I don't
// know the exact reason for this, but this requires the script to pull the
// followers and following sets through multiple requests. Would have been
// very convenient to just ask for '.first(10000)' and wait for the sets to
// be sent in one go. The official API seems to be doing this, as there is
// the /users/self/follows relationship endpoint that, I assume, returns all
// the following set. It doesn't take a count parameter, which probably
// makes sense for this case. There's also no proper filtering either. The
// unofficial API is clearly superior to the official one (!).
// The URLSearchParams object is used, as the FormData objects are sent as
// 'multipart/form-data' and the endpoint accepts 'x-www-form-urlencoded'
// only. Also, 'window._sharedData.config.viewer.id' contains the user ID
// needed for the queries.
var urlFormDataFollowers = new URLSearchParams();
urlFormDataFollowers.set('q', 'ig_user(' + window._sharedData.config.viewer.id + ') {' +
' followed_by.first(10) {' +
' page_info {' +
' end_cursor,' +
' has_next_page' +
' },' +
' nodes {' +
' id,' +
' is_verified,' +
' followed_by_viewer,' +
' requested_by_viewer,' +
' full_name,' +
' profile_pic_url,' +
' username' +
' }' +
' }' +
'}');
var urlFormDataFollowing = new URLSearchParams();
urlFormDataFollowing.set('q', 'ig_user(' + window._sharedData.config.viewer.id + ') {' +
' follows.first(10) {' +
' page_info {' +
' end_cursor,' +
' has_next_page' +
' },' +
' nodes {' +
' id,' +
' is_verified,' +
' followed_by_viewer,' +
' requested_by_viewer,' +
' full_name,' +
' profile_pic_url,' +
' username' +
' }' +
' }' +
'}');
window.followers = [];
window.followersFinished = false;
// The following traverse functions are to go through the responses by the
// end cursors. As mentioned before, it's not possible to get all the
// followers or following in one go.
function traverseFollowers(end_cursor) {
if (!end_cursor) { // Begins the search
debug && console.log("Traversing Followers, no cursor.");
queryURLFormData(urlFormDataFollowers, function(data) {
window.followers = window.followers.concat(data.followed_by.nodes);
if (data.followed_by.page_info.has_next_page) { // Continue if has more followers remaining
traverseFollowers(data.followed_by.page_info.end_cursor);
}
});
} else {
debug && console.log("Traversing Followers, has cursor...", end_cursor);
// Query is modified to include the next cursor to continue the search
urlFormDataFollowers.set('q', 'ig_user(' + window._sharedData.config.viewer.id + ') {' +
' followed_by.after(' + end_cursor + ', 10) {' +
' page_info {' +
' end_cursor,' +
' has_next_page' +
' },' +
' nodes {' +
' id,' +
' is_verified,' +
' followed_by_viewer,' +
' requested_by_viewer,' +
' full_name,' +
' profile_pic_url,' +
' username' +
' }' +
' }' +
'}');
queryURLFormData(urlFormDataFollowers, function(data) {
window.followers = window.followers.concat(data.followed_by.nodes);
if (data.followed_by.page_info.has_next_page) { // Continue if has more followers remaining
traverseFollowers(data.followed_by.page_info.end_cursor);
} else {
window.followersFinished = true;
calculateComplements();
}
});
}
}
traverseFollowers();
window.following = [];
window.followingFinished = false;
function traverseFollowing(end_cursor) {
if (!end_cursor) {
debug && console.log("Traversing Following, no cursor.");
queryURLFormData(urlFormDataFollowing, function(data) {
window.following = window.following.concat(data.follows.nodes);
if (data.follows.page_info.has_next_page) {
traverseFollowing(data.follows.page_info.end_cursor);
}
});
} else {
debug && console.log("Traversing Following, has cursor...", end_cursor);
urlFormDataFollowing.set('q', 'ig_user(' + window._sharedData.config.viewer.id + ') {' +
' follows.after(' + end_cursor + ', 10) {' +
' page_info {' +
' end_cursor,' +
' has_next_page' +
' },' +
' nodes {' +
' id,' +
' is_verified,' +
' followed_by_viewer,' +
' requested_by_viewer,' +
' full_name,' +
' profile_pic_url,' +
' username' +
' }' +
' }' +
'}');
queryURLFormData(urlFormDataFollowing, function(data) {
window.following = window.following.concat(data.follows.nodes);
if (data.follows.page_info.has_next_page) {
traverseFollowing(data.follows.page_info.end_cursor);
} else {
window.followingFinished = true;
calculateComplements();
}
});
}
}
traverseFollowing();
// This function calculates the complements of the two finite sets of
// 'followers' and 'following' to create the human-readable "I'm not
// following them, but they are following me" and "I'm following them, but
// they aren't following me" sets.
function calculateComplements() {
// Asynchronous calculation done only when both sets are complete
if (window.followersFinished && window.followingFinished) {
debug && console.log("Calculating complements...", window.followers, window.following);
window.notFollowingMyFollowers = window.followers.filter(function(follower) {
for (var i = 0; i < window.following.length; i++) {
if (window.following[i].id === follower.id) {
return false; // Drop if shared
}
}
return true;
});
window.notFollowingBack = window.following.filter(function(following) {
for (var i = 0; i < window.followers.length; i++) {
if (window.followers[i].id === following.id) {
return false;
}
}
return true;
});
debug && console.log("Calculated complements!");
if (usernamesOnly) { // Map the arrays to just the usernames
window.notFollowingMyFollowers = window.notFollowingMyFollowers.map(function(follower) {
return follower.username;
});
window.notFollowingBack = window.notFollowingBack.map(function(following) {
return following.username;
});
}
console.info("I'm not following them, but they are following me", window.notFollowingMyFollowers);
console.info("I'm following them, but they aren't following me", window.notFollowingBack);
}
}
})(false, true);
// First argument 'debug' posts certain network information, second argument
// 'usernamesOnly' returns only the usernames instead of profile information
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment