// ==UserScript==
// @name Instagram Source Opener
// @version 0.7
// @description Allows the user to open the original source of an instagram post, story or profile picture. No jQuery
// @author jomifepe
// @icon https://www.instagram.com/favicon.ico
// @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
// @include https://www.instagram.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @namespace https://gf.qytechs.cn/users/192987
// ==/UserScript==
(function() {
"use strict";
/* this script relies a lot on class names, I'll keep an eye on changes */
const IG_C_STORY_CONTAINER = "yS4wN";
const IG_C_STORY_MEDIA_CONTAINER = "qbCDp";
const IG_C_POST_IMG = "FFVAD";
const IG_C_POST_VIDEO = "tWeCl";
const IG_C_SINGLE_POST_CONTAINER = "JyscU";
const IG_C_MULTI_POST_SCROLLER = "MreMs";
const IG_C_MULTI_POST_LIST_ITEM = "_-1_m6";
const IG_S_POSTS_CONTAINER = ".cGcGK > div > div";
const IG_S_POST_BUTTONS = ".eo2As > section";
const IG_C_PROFILE_PIC_CONTAINER = "RR-M-";
const IG_C_PRIVATE_PROFILE_PIC_CONTAINER = "M-jxE";
const IG_C_PRIVATE_PIC_IMG_CONTAINER = "_2dbep";
const IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER = "IalUJ";
const IG_C_PROFILE_CONTAINER = "v9tJq";
const IG_C_PROFILE_USERNAME_TITLE = "fKFbl";
const C_BTN_STORY = "iso-story-btn";
const C_BTN_STORY_CONTAINER = "iso-story-container";
const C_POST_WITH_BUTTON = "iso-post";
const C_BTN_POST_OUTER_SPAN = "iso-post-container";
const C_BTN_POST = "iso-post-btn";
const C_BTN_POST_INNER_SPAN = "iso-post-span";
const C_BTN_PROFILE_PIC_CONTAINER = "iso-profile-pic-container";
const C_BTN_PROFILE_PIC = "iso-profile-picture-btn";
const C_BTN_PROFILE_PIC_SPAN = "iso-profile-picture-span";
const getIgUserInfoApiUrl = (userID) => `https://i.instagram.com/api/v1/users/${userID}/info/`;
/* injects the needed CSS into DOM */
injectStyles();
/* triggered whenever a new instagram post is loaded on the feed */
document.arrive(`${IG_S_POSTS_CONTAINER} article`, (node) => {
generatePostButton(node);
});
/* triggered whenever a single post is opened (on a profile) */
document.arrive(`.${IG_C_SINGLE_POST_CONTAINER}`, (node) => {
generatePostButton(node);
});
/* triggered whenever a story is opened */
document.arrive(`.${IG_C_STORY_CONTAINER}`, (node) => {
generateStoryButton(node);
});
/* triggered a profile is loaded */
document.arrive(`.${IG_C_PROFILE_CONTAINER}`, (node) => {
generateProfilePictureButton(node);
});
window.onload = ((e) => {
/* aditional check because sometimes the arive functions aren't triggered when the page is hard reloaded */
if (window.location.href.indexOf("instagram.com/p/") > -1) {
let node = document.querySelector(`.${IG_C_SINGLE_POST_CONTAINER}`);
if (node != null) {
generatePostButton(node);
}
} else if (window.location.href.indexOf("/stories/") > -1) {
let node = document.querySelector(`.${IG_C_STORY_CONTAINER}`);
if (node == null) {
generateStoryButton(node);
}
}
})
function generateStoryButton(node) {
/* exits if the story button already exists */
if (elementExistsInNode(`.${C_BTN_STORY_CONTAINER}`, node)) return;
try {
let buttonStoryContainer = document.createElement("span");
let buttonStory = document.createElement("button");
buttonStoryContainer.classList.add(C_BTN_STORY_CONTAINER);
buttonStory.classList.add(C_BTN_STORY);
buttonStoryContainer.setAttribute("title", "Open source");
buttonStory.addEventListener("click", () => openStoryContent(node));
buttonStoryContainer.appendChild(buttonStory);
node.appendChild(buttonStoryContainer);
} catch (err) {
showDefaultErrorMessage(err);
}
}
function generatePostButton(node) {
/* exits if the post button already exists */
if (elementExistsInNode(`.${C_BTN_POST_OUTER_SPAN}`, node)) return;
try {
let buttonsContainer = node.querySelector(IG_S_POST_BUTTONS);
let newElementOuterSpan = document.createElement("span");
let newElementButton = document.createElement("button");
let newElementInnerSpan = document.createElement("span");
newElementOuterSpan.classList.add(C_BTN_POST_OUTER_SPAN);
newElementButton.classList.add(C_BTN_POST);
newElementInnerSpan.classList.add(C_BTN_POST_INNER_SPAN);
newElementOuterSpan.setAttribute("title", "Open source");
newElementButton.addEventListener("click", () => openPostSourceFromSrcAttribute(node));
newElementButton.appendChild(newElementInnerSpan);
newElementOuterSpan.appendChild(newElementButton);
buttonsContainer.appendChild(newElementOuterSpan);
node.classList.add(C_POST_WITH_BUTTON);
} catch (err) {
showDefaultErrorMessage(err);
}
}
function generateProfilePictureButton(node) {
/* exits if the profile picture button already exists */
if (elementExistsInNode(`.${C_BTN_PROFILE_PIC_CONTAINER}`, node)) return;
try {
let profilePictureContainer = node.querySelector(`.${IG_C_PROFILE_PIC_CONTAINER}`);
/* if the profile is private and the user isn't following or isn't logged in */
if (!profilePictureContainer) {
profilePictureContainer = node.querySelector(`.${IG_C_PRIVATE_PROFILE_PIC_CONTAINER}`);
}
let newElementOuterSpan = document.createElement("span");
let newElementButton = document.createElement("button");
let newElementInnerSpan = document.createElement("span");
newElementOuterSpan.setAttribute("title", "View profile picture");
newElementButton.addEventListener("click", e => {
e.stopPropagation();
openProfilePictureSource();
});
newElementOuterSpan.classList.add(C_BTN_PROFILE_PIC_CONTAINER);
newElementButton.classList.add(C_BTN_PROFILE_PIC);
newElementInnerSpan.classList.add(C_BTN_PROFILE_PIC_SPAN);
newElementButton.appendChild(newElementInnerSpan);
newElementOuterSpan.appendChild(newElementButton);
profilePictureContainer.appendChild(newElementOuterSpan);
} catch (error) {
logError(error);
}
}
function openStoryContent(node) {
try {
let container = node.querySelector(`.${IG_C_STORY_MEDIA_CONTAINER}`);
let video = container.querySelector("video");
let image = container.querySelector("img");
if (video) {
let videoElement = video.querySelector("source");
let videoSource = videoElement ? videoElement.getAttribute("src") : null;
if (!videoSource) {
throw "Failed to open video source";
}
window.open(videoSource, "_blank");
} else if (image) {
let imageSource = image.getAttribute("src");
if (!imageSource) {
throw "Failed to open image source";
}
window.open(imageSource, "_blank");
} else {
throw "Failed to open media source"
}
} catch (err) {
showDefaultErrorMessage(err);
}
}
function openPostSourceFromSrcAttribute(node) {
let nodeListItems = node.querySelectorAll(`.${IG_C_MULTI_POST_LIST_ITEM}`);
try {
if (/* is multi post */ nodeListItems.length != 0) {
let scroller = node.querySelector(`.${IG_C_MULTI_POST_SCROLLER}`);
let scrollerOffset = Math.abs((() => {
let scrollerStyles = window.getComputedStyle(scroller);
return parseInt(scrollerStyles.getPropertyValue("transform").split(",")[4]);
})());
let mediaIndex = 0;
if (scrollerOffset != 0) {
let totalWidth = 0;
nodeListItems.forEach(item => {
let itemStyles = window.getComputedStyle(item);
totalWidth += parseInt(itemStyles.getPropertyValue("width"));
});
mediaIndex = ((scrollerOffset * nodeListItems.length) / totalWidth);
}
openPostMediaSource(nodeListItems[mediaIndex]);
} else /* is single post */ {
openPostMediaSource(node);
}
} catch (err) {
showDefaultErrorMessage(err);
}
}
function openPostMediaSource(nodeToSearchForMedia) {
let image = nodeToSearchForMedia.querySelector(`.${IG_C_POST_IMG}`);
let video = nodeToSearchForMedia.querySelector(`.${IG_C_POST_VIDEO}`);
if (!image && !video) {
throw "Failed to open source, no media found";
}
window.open((video || image).getAttribute("src"), "_blank");
}
function openProfilePictureSource() {
let defaultErrorHandler = error => {
document.body.style.cursor = "default";
alert("Couldn't get profile picture source");
logError(`Failed to get profile picture source: ${error}`);
}
try {
let openImageFromUserInfo = response => {
let hdImageURL = response.hd_profile_pic_url_info;
if (hdImageURL != null) {
window.open(hdImageURL.url, "_blank");
}
document.body.style.cursor = "default";
};
let openImageFromUpdatedSharedData = () => {
getUpdatedUserSharedData()
.then(response => {
getUserInfoFromAPI(response.id)
.then(openImageFromUserInfo)
.catch(defaultErrorHandler);
})
.catch(defaultErrorHandler);
};
let openImageFromUserInfoAPI = userData => {
getUserInfoFromAPI(userData.id)
.then(openImageFromUserInfo)
.catch(error => {
let sharedDataImageURL = userData.profile_pic_url_hd;
if (sharedDataImageURL) {
window.open(sharedDataImageURL, "_blank");
} else {
defaultErrorHandler(error);
}
});
};
let openImageFromFreshHTMLPage = () => {
getUserInfoFromFreshHTMLPage()
.then(openImageFromUserInfoAPI)
.catch(defaultErrorHandler);
};
let pageUsername = document.querySelector(`.${IG_C_PROFILE_USERNAME_TITLE}`).innerText;
let profilePageData = _sharedData.entry_data.ProfilePage;
document.body.style.cursor = "wait";
/* if sharedData has any user information */
if (profilePageData) {
let userSharedData = profilePageData[0].graphql.user;
/* if sharedData is correct */
if (pageUsername === userSharedData.username) {
/* getting user info from the api */
openImageFromUserInfoAPI(userSharedData);
/* if the user is logged in */
} else if (_sharedData.config.viewer != null) {
/* querying graphql directly to get user info*/
openImageFromUpdatedSharedData();
} else {
openImageFromFreshHTMLPage();
}
} else {
openImageFromFreshHTMLPage();
}
} catch (error) {
defaultErrorHandler(error);
}
}
/**
* This function parses a whole HTML page as a last attempt to get the user id
* It's only used when:
* - The profile page is private
* - The user isn't logged in
* - The sharedData variable, which holds the profile user's id, isn't correct
*/
function getUserInfoFromFreshHTMLPage() {
return new Promise((resolve, reject) => {
httpGETRequest(window.location, false)
.then(response => {
try {
let parser = new DOMParser();
let doc = parser.parseFromString(response, "text/html");
let allScripts = doc.querySelectorAll("script");
for (let i = 0; i < allScripts.length; i++) {
if (/window._sharedData/.test(allScripts[i].innerText)) {
let extractedJSON = /window._sharedData = (.+)/.exec(allScripts[i].innerText)[1];
extractedJSON = extractedJSON.slice(0, -1);
let sharedData = JSON.parse(extractedJSON);
let userInfo = sharedData.entry_data.ProfilePage[0].graphql.user;
resolve(userInfo);
break;
}
}
} catch (error) {
reject(error);
}
})
.catch(error => reject(error));
});
}
function getUserInfoFromAPI(userId) {
return new Promise((resolve, reject) => {
httpGETRequest(getIgUserInfoApiUrl(userId))
.then(response => {
let userInfo = response.user;
resolve(userInfo);
})
.catch(error => reject(error))
})
}
function getUpdatedUserSharedData() {
return new Promise((resolve, reject) => {
httpGETRequest(`${window.location}?__a=1`)
.then(response => {
let userSharedData = response.graphql.user;
resolve(userSharedData);
})
.catch(error => reject(error))
});
}
function httpGETRequest(url, parseToJSON = true) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: res => {
if (res.status === 200) {
let response = res.responseText;
if (parseToJSON) {
response = JSON.parse(res.responseText);
}
resolve(response);
} else {
reject(`Status Code ${res.status} ${res.statusText.length > 0 ?
', ' + res.statusText : ''}`);
}
},
onerror: error => reject(error),
ontimeout: () => reject("Request Timeout"),
onabort: () => reject("Aborted")
})
})
}
function elementExistsInNode(selector, node) {
return (node.querySelector(selector) != null);
}
function injectStyles() {
let b64icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiAxwDOBTFNFQBAAABKklEQVQ4y6WTvUoDQRSFvwkbxCqijY2okEIEixQpBMHKykIFC8H/yiew8Rl8i4ClCoJYWGkhaGXjrkmTbsUiTQoVf45Ndp1lZ8eIp7vnnnPvnBkG/gjjb2uAOcoW0fplnvaVRccAaIFZhnPqW3OkMa4Zz84o60RunAFoQm2bDDhgmSsOHad7BjBtrXFjb3jUi0Y8KUYV2hvQly77kH/qKTFIF33Id5MsHoMl30njdwoNlnw75SqaLDC45EnLYbDkW/lZOYMl3wRQTTW/4bQn3+jVoUK/YUqxPrSe1pGin26QD2wizVM15+7LDlykadIseswSbwzhgUpUeLWJO0nTHsOSpIa1XSsc06VR8PnqrGKom3t7xp66KkasxUw+AA0y4/iiADEP5p3/4BuEXi9gkPrfQgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0wMy0yOFQwMzo1NjoyMCswMjowMO7sj9MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMDMtMjhUMDM6NTY6MjArMDI6MDCfsTdvAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg==";
let styles = [
`.${C_BTN_POST_OUTER_SPAN}{margin-left:10px;margin-right:-10px;}`,
`.${C_BTN_POST}{outline:none;-webkit-box-align:center;align-items:center;background:0;border:0;cursor:pointer;display:flex;-webkit-box-flex:0;flex-grow:0;-webkit-box-pack:center;justify-content:center;min-height:40px;min-width:40px;padding:0;}`,
`.${C_BTN_PROFILE_PIC}{outline:none;background-color:white;border:0;cursor:pointer;display:flex;-webkit-box-flex:0;flex-grow:0;-webkit-box-pack:center;justify-content:center;min-height:40px;min-width:40px;padding:0;border-radius:50%;transition:background-color .5s ease;-webkit-transition:background-color .5s ease;}`,
`.${C_BTN_PROFILE_PIC}:hover{background-color:#D0D0D0;transition:background-color .5s ease;-webkit-transition:background-color .5s ease;}`,
`.${C_BTN_POST_INNER_SPAN},.${C_BTN_PROFILE_PIC_SPAN}{display:block;background-repeat:no-repeat;background-position:100%-26px;height:24px;width:24px;background-image:url(/static/bundles/base/sprite_glyphs.png/4b550af4600d.png);cursor:pointer;}`,
`.${C_BTN_STORY}{border:none;position:fixed;top:0;right:0;margin:20px;cursor:pointer;width:24px;height:24px;background-color:transparent;background-image:url(${b64icon})}`,
`.${C_BTN_PROFILE_PIC_CONTAINER}{transition:.5s ease;opacity:0;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);text-align:center}`,
`.${IG_C_PRIVATE_PIC_IMG_CONTAINER}>img{transition:.5s ease;backface-visibility:hidden;}`,
`.${IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER}>img{transition:.5s ease;backface-visibility:hidden;}`,
`.${IG_C_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC_CONTAINER}{opacity:1}`,
`.${IG_C_PRIVATE_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC_CONTAINER}{opacity:1}`
];
styles.forEach((style) => GM_addStyle(style));
}
function showDefaultErrorMessage(error) {
alert(`${error}\n\nSorry for the inconvenience, the developer is on it!`);
}
function logError(error) {
console.error(`Instagram Source Opener:\n${error}`);
}
})();