// ==UserScript==
// @name Instagram Source Opener
// @version 0.8
// @description Open the original source of an IG 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"
const LOGGING_ENABLED = false
/* this script relies a lot on class names, I'll keep an eye on changes */
/* Instagram classes and selectors */
const IG_C_STORY_CONTAINER = "yS4wN",
IG_C_STORY_MEDIA_CONTAINER = "qbCDp",
IG_C_POST_IMG = "FFVAD",
IG_C_POST_VIDEO = "tWeCl",
IG_C_SINGLE_POST_CONTAINER = "JyscU",
IG_C_MULTI_POST_SCROLLER = "MreMs",
IG_C_MULTI_POST_LIST_ITEM = "_-1_m6",
IG_C_POST_CONTAINER = "_8Rm4L",
IG_S_POST_BUTTONS = ".eo2As > section",
IG_C_PROFILE_PIC_CONTAINER = "RR-M-",
IG_C_PRIVATE_PROFILE_PIC_CONTAINER = "M-jxE",
IG_C_PRIVATE_PIC_IMG_CONTAINER = "_2dbep",
IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER = "IalUJ",
IG_C_PROFILE_CONTAINER = "v9tJq",
IG_C_PROFILE_USERNAME_TITLE = "fKFbl",
IG_C_INSTAGRAM_POST_BLOCKER = "_9AhH0"
/* Custom classes and selectors */
const C_BTN_STORY = "iso-story-btn",
C_BTN_STORY_CONTAINER = "iso-story-container",
C_POST_WITH_BUTTON = "iso-post",
C_BTN_POST_OUTER_SPAN = "iso-post-container",
C_BTN_POST = "iso-post-btn",
C_BTN_POST_INNER_SPAN = "iso-post-span",
C_BTN_PROFILE_PIC_CONTAINER = "iso-profile-pic-container",
C_BTN_PROFILE_PIC = "iso-profile-picture-btn",
C_BTN_PROFILE_PIC_SPAN = "iso-profile-picture-span"
const S_IG_POST_CONTAINER_WITHOUT_BUTTON = `.${IG_C_POST_CONTAINER}:not(.${C_POST_WITH_BUTTON})`
let isStoryKeyBindingSetup = false,
isSinglePostKeyBindingSetup = false,
isProfileKeyBindingSetup = false
/* injects the needed CSS into DOM */
injectStyles()
/* triggered whenever a new instagram post is loaded on the feed */
document.arrive(S_IG_POST_CONTAINER_WITHOUT_BUTTON, node => generatePostButton(node))
/* triggered whenever a single post is opened (on a profile) */
document.arrive(`.${IG_C_SINGLE_POST_CONTAINER}`, node => {
generatePostButton(node)
setupSinglePostKeyBindings()
})
/* triggered whenever a story is opened */
document.arrive(`.${IG_C_STORY_CONTAINER}`, node => {
generateStoryButton(node)
setupStoryKeyBindings()
})
/* triggered whenever a profile page is loaded */
document.arrive(`.${IG_C_PROFILE_CONTAINER}`, node => {
generateProfilePictureButton(node)
setupProfileKeyBindings()
})
/* triggered whenever a single post is closed (on a profile) */
document.leave(`.${IG_C_SINGLE_POST_CONTAINER}`, node => removeSinglePostKeyBindings())
/* triggered whenever a story is closed */
document.leave(`.${IG_C_STORY_CONTAINER}`, node => removeStoryKeyBindings())
/* triggered whenever a profile page is left */
document.leave(`.${IG_C_PROFILE_CONTAINER}`, node => removeProfileKeyBindings())
/**
* Window load callback
* Checks if there are relevant nodes already loaded in DOM and performs the corresponding actions
*/
window.onload = (e => {
if (/* is on post feed */ window.location.pathname === '/') {
let postArticles = document.querySelectorAll(S_IG_POST_CONTAINER_WITHOUT_BUTTON)
postArticles.forEach(node => generatePostButton(node))
} else if (isOnSinglePostPage()) {
let node = document.querySelector(`.${IG_C_SINGLE_POST_CONTAINER}`)
if (node != null) {
generatePostButton(node)
}
setupSinglePostKeyBindings()
} else if (isOnStoryPage()) {
let node = document.querySelector(`.${IG_C_STORY_CONTAINER}`)
if (node == null) {
generateStoryButton(node)
}
setupStoryKeyBindings()
} else if (isOnProfilePage()) {
let node = document.querySelector(`.${IG_C_PROFILE_CONTAINER}`)
if (node != null) {
generateProfilePictureButton(node)
}
setupProfileKeyBindings()
}
})
/**
* Appends new elements to DOM containing the story source opening button
* @param {Object} node DOM element 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 (exception) {
logError("Failed to generate story button", exception)
}
}
/**
* Appends new elements to DOM containing the post source opening button
* @param {Object} node DOM element node
*/
function generatePostButton(node) {
/* exits if the post button already exists */
if (elementExistsInNode(`.${C_BTN_POST_OUTER_SPAN}`, node)) return
try {
/* removes the div that's blocking the img element on a post */
let blocker = node.querySelector(`.${IG_C_INSTAGRAM_POST_BLOCKER}`)
if (blocker) blocker.parentNode.removeChild(blocker)
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 (exception) {
logError("Failed to generate post button", exception)
}
}
/**
* Appends new elements to DOM containing the profile picture source opening button
* @param {Object} node DOM element node
*/
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", "Open full size picture")
newElementButton.addEventListener("click", e => {
e.stopPropagation()
openProfilePicture()
})
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)
}
}
/**
* Gets the story source url from the src attribute on the node and opens it in a new tab
* @param {Object} node DOM element node
*/
function openStoryContent(node = null) {
try {
let container = (node || document).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 "Video source isn't available"
}
window.open(videoSource, "_blank")
} else if (image) {
let imageSource = image.getAttribute("src")
if (!imageSource) {
throw "Image source isn't available"
}
window.open(imageSource, "_blank")
} else {
throw "Story media isn't available"
}
} catch (exception) {
showAndLogError("Failed to open story source", exception)
}
}
/**
* Gets the source url of a post from the src attribute on the node and opens it in a new tab
* @param {Object} node DOM element node
*/
function openPostSourceFromSrcAttribute(node = null) {
/* if is on single post page and the node is null, the picture container can be found, since there's only one */
if (node == null) {
node = document.querySelector(`.${IG_C_SINGLE_POST_CONTAINER}`)
if (node == null) return
}
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 (exception) {
showAndLogError("Failed o open post source", exception)
}
}
/**
* Gets the source url of a post from the src attribute on the node and opens it in a new tab
* @param {Object} node DOM element node
*/
function openPostMediaSource(node) {
let image = node.querySelector(`.${IG_C_POST_IMG}`)
let video = node.querySelector(`.${IG_C_POST_VIDEO}`)
if (!image && !video) {
throw "Failed to open source, no media found"
}
window.open((video || image).getAttribute("src"), "_blank")
}
/**
* Tries to get the source URL of the user's profile picture using multiple methods, including 3rd party websites
* Opens the image in a new tab or shows an alert if it doesn't find any URL
*/
async function openProfilePicture() {
let pageUsername = document.querySelector(`.${IG_C_PROFILE_USERNAME_TITLE}`).innerText
let profilePageData = _sharedData.entry_data.ProfilePage
var pictureUrl = null
document.body.style.cursor = "wait"
logMessage("Trying to get user's profile picture from 3rd party websites", true)
/* trying to get the picture from instadp.org */
try {
logMessage("Trying to get user's profile picture from instadp.org")
pictureUrl = await getProfilePictureFromInstadpDotOrg(pageUsername)
if (!(await pictureUrl)) logError("No profile picture url found on instadp.org response")
} catch (error) {
logError("Couldn't get picture from instadp.org", error)
}
if (!pictureUrl) {
/* trying to get the picture from instadp.com */
try {
logMessage("Trying to get user's profile picture from instadp.com")
pictureUrl = await getProfilePictureFromInstadpDotCom(pageUsername)
if (!(await pictureUrl)) logError("No profile picture url found on instadp.com response")
} catch (error) {
logError("Couldn't get picture from instadp.com", error)
}
}
if (!pictureUrl) {
/* trying to get the picture from izuum.com */
try {
logMessage("Trying to get user's profile picture from izuum.com")
pictureUrl = await getProfilePictureFromIzuum(pageUsername)
if (!(await pictureUrl)) logError("No profile picture url found on izuum.com response")
} catch (error) {
logError("Couldn't get picture from izuum.com", error)
}
}
if (!pictureUrl) {
/* trying to get the picture from existing data on the user's profile */
try {
logMessage("Trying to get user's profile picture from existing user data", true)
pictureUrl = await getPictureFromExistingData(pageUsername, profilePageData)
if (!(await pictureUrl)) logError("No profile picture url found on any existing data")
} catch (error) {
logError("Couldn't get picture from existing data on user's profile (3 methods failed)", error)
}
}
document.body.style.cursor = "default"
if (pictureUrl) {
logMessage("Profile picture found, opening in a new tab")
window.open(pictureUrl, "_blank")
} else {
showMessage("Couldn't get user's profile picture")
}
}
/**
* Adds event listener(s) to the current document meant to handle key presses on a single post page
*/
function setupSinglePostKeyBindings() {
if (!isSinglePostKeyBindingSetup) {
logMessage("Added single post opening key binding")
document.addEventListener('keyup', handleSinglePostKeyUp)
isSinglePostKeyBindingSetup = true
}
}
/**
* Adds event listener(s) to the current document meant to handle key presses on a story page
*/
function setupStoryKeyBindings() {
if (!isStoryKeyBindingSetup) {
logMessage("Added story key bindings")
document.addEventListener('keyup', handleStoryKeyUp)
isStoryKeyBindingSetup = true
}
}
/**
* Adds event listener(s) to the current document meant to handle key presses on a profile page
*/
function setupProfileKeyBindings() {
if (!isProfileKeyBindingSetup) {
logMessage("Added profile key bindings")
document.addEventListener('keyup', handleProfileKeyUp)
isProfileKeyBindingSetup = true
}
}
/**
* Removes the previously added event listener(s) meant to handle key presses on a single post page
*/
function removeSinglePostKeyBindings() {
if (isSinglePostKeyBindingSetup) {
logMessage("Removed single post key bindings")
document.removeEventListener('keyup', handleSinglePostKeyUp)
isStoryKeyBindingSetup = false
}
}
/**
* Removes the previously added event listener(s) meant to handle key presses on a story page
*/
function removeStoryKeyBindings() {
if (isStoryKeyBindingSetup) {
logMessage("Removed story key bindings")
document.removeEventListener('keyup', handleStoryKeyUp)
isStoryKeyBindingSetup = false
}
}
/**
* Removes the previously added event listener(s) meant to handle key presses on a profile page
*/
function removeProfileKeyBindings() {
if (isProfileKeyBindingSetup) {
logMessage("Removed profile key bindings")
document.removeEventListener('keyup', handleProfileKeyUp)
isProfileKeyBindingSetup = false
}
}
/**
* Handles key up events on a story page
* @param {Object} e Event object
*/
function handleStoryKeyUp(e) {
if (e.code === "KeyO") {
if (isOnStoryPage()) {
if (/* alt is active */ event.getModifierState("Alt")) {
logMessage("Detected source opening shortcut on a story page")
openStoryContent()
}
}
}
}
/**
* Handles key up events on a single post page
* @param {Object} e Event object
*/
function handleSinglePostKeyUp(e) {
if (e.code === "KeyO") {
if (isOnSinglePostPage()) {
if (/* alt is active */ event.getModifierState("Alt")) {
logMessage("Detected source opening shortcut on a single post page")
openPostSourceFromSrcAttribute()
}
}
}
}
/**
* Handles key up events on a profile page
* @param {Object} e Event object
*/
function handleProfileKeyUp(e) {
if (e.code === "KeyP") {
if (!isOnStoryPage()) {
if (/* alt is active */ event.getModifierState("Alt")) {
logMessage("Detected profile picture opening shortcut on a profile page")
openProfilePicture()
}
}
}
}
/**
* Tries to get the user's profile picture URL from sharedData and graphql API
* Picture URLs from shared data are usually low-res versions
* @param {string} username
* @param {Object} existingData
* @returns {Promise} Promise object, when resolved contains the picture URL
*/
async function getPictureFromExistingData(username, existingData) {
return new Promise(async (resolve, reject) => {
var pictureUrl = null
/* trying to get the from current page's sharedData variable */
try {
logMessage("Trying to get user's profile picture from current page's sharedData")
let userSharedData = existingData[0].graphql.user
/* if sharedData is correct */
if (userSharedData.username === username) {
pictureUrl = userSharedData.profile_pic_url_hd
if (!pictureUrl) logError("No profile picture url found on current page's sharedData")
} else {
logError("Current sharedData is incorrect, discarding url")
}
} catch (error) {
logError("Couldn't get url from current page's sharedData", error)
}
if (!pictureUrl) {
/* trying to get the picture from user data graphql API (?__a=1) */
try {
logMessage("Trying to get user's profile picture from user data graphql API (?__a=1)")
pictureUrl = await getProfilePictureFromUpdatedSharedData()
if (!(await pictureUrl)) logError("No profile picture url found on user data graphql API (?__a=1)")
} catch (error) {
logError("Couldn't get picture from user data graphql API (?__a=1)", error)
}
}
if (!pictureUrl) {
/* last resort: trying to get the picture from an updated HTML profile page */
try {
logMessage("Trying to get user's profile picture from updated HTML profile page")
pictureUrl = await getProfilePictureFromUpdatedHTMLPage()
if (!(await pictureUrl)) logError("No profile picture url found on updated HTML page")
} catch (error) {
logError("Couldn't get picture from updated HTML page", error)
}
}
pictureUrl ? resolve(pictureUrl) : reject()
})
}
/**
* Parses a whole HTML page in order to get the user's profile picture URL
* @returns {Promise} Promise object, when resolved returns the user's profile picture URL
* and when rejected contains an exception error object
*/
function getProfilePictureFromUpdatedHTMLPage() {
getProfilePictureFromUp
return new Promise((resolve, reject) => {
httpGETRequest(window.location, false)
.then(response => {
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.profile_pic_url_hd
resolve(userInfo)
break
}
}
})
.catch(error => reject(error))
})
}
/**
* Requests user information from the Instagram API
* @deprecated This endpoint was almost completely shut down by instagram and only works with a different user agent and provides low-res pictures
* @param {number} userId
* @returns {Promise} Promise object, when resolved contains an object with the user's information
* and when rejected contains an exception error object
*/
function getUserFromUserInfoAPI(userId) {
return new Promise((resolve, reject) => {
httpGETRequest(`https://i.instagram.com/api/v1/users/${userId}/info/`)
.then(response => resolve(response.user))
.catch(error => reject(error))
})
}
/**
* Requests the user profile page data from graphql in order to get its profile picture URL
* @returns {Promise} Promise object, when resolved contains the user's profile picture URL
* and when rejected contains an exception error object
*/
function getProfilePictureFromUpdatedSharedData() {
return new Promise((resolve, reject) => {
httpGETRequest(`${window.location}?__a=1`)
.then(response => resolve(response.graphql.user.profile_pic_url_hd))
.catch(error => reject(error))
})
}
/**
* Performs a request to instadp.com and extracts the profile picture URL from the HTML response
* @todo Adapt to be able to get pictures from a user that was never searched on their website
* @param {string} username The user's Instagram username
* @returns {Promise} Promise object, when resolved contains the user's profile picture URL
* and when rejected contains an exception error object or a null if no URL was found
*/
function getProfilePictureFromInstadpDotCom(username) {
/*
Instadp.com has a different process and requires a POST, probably to populate their database
If you ever searched for a user on their website, this request succeeds, otherwise it fails
*/
return new Promise((resolve, reject) => {
httpGETRequest(`https://www.instadp.com/fullsize/${username}`, false)
.then(response => {
let urls = extractUrlsFromString(response)
let instagramUrls = urls.filter(u =>
u.includes("cdninstagram") && !u.includes("s150x150"))
if (instagramUrls.length > 0) {
resolve(instagramUrls[instagramUrls.length - 1])
} else {
reject()
}
})
.catch(error => reject(error))
})
}
/**
* Performs a request to instadp.org and extracts the profile picture URL from the HTML response
* @param {string} username The user's Instagram username
* @returns {Promise} Promise object, when resolved contains the user's profile picture URL and
* when rejected contains an exception error object or a null if no URL was found
*/
function getProfilePictureFromInstadpDotOrg(username) {
return new Promise((resolve, reject) => {
let headers = {"Content-Type": "application/x-www-form-urlencoded"}
let data = `username=${username}`
httpPOSTRequest('https://instadp.org/', headers, data, false)
.then(response => {
let urls = extractUrlsFromString(response)
let instagramUrls = urls.filter(u => u.includes("cdninstagram"))
if (instagramUrls.length > 0) {
resolve(instagramUrls[0])
} else {
reject()
}
})
.catch(error => reject(error))
})
}
/**
* Performs a request to izuum.com and extracts the profile picture URL from the HTML response
* @param {string} username The user's Instagram username
* @returns {Promise} Promise object, when resolved contains the user's profile picture URL
* and when rejected contains an exception error object or a null if no URL was found
*/
function getProfilePictureFromIzuum(username) {
return new Promise((resolve, reject) => {
let headers = {"Content-Type": "application/x-www-form-urlencoded"}
let data = `submit=${username}`
httpPOSTRequest('http://izuum.com/index.php', headers, data, false)
.then(response => {
let urls = extractUrlsFromString(response)
let instagramUrls = urls.filter(u => u.includes("cdninstagram"))
let cleanUrls = instagramUrls.map(u => u.replace(/amp;/g, ''))
if (cleanUrls.length > 0) {
resolve(cleanUrls[0])
} else {
reject()
}
})
.catch(error => reject(error))
})
}
/**
* Performs an HTTP GET request using the GM_xmlhttpRequest function
* @param {string} url
* @param {boolean} [parseToJSON = true] default true
* @returns {Promise} Promise object, when resolved contains the response text and
* when rejected contains a message or an exception error object
*/
function httpGETRequest(url, parseToJSON = true) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
timeout: 10000,
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")
})
})
}
/**
* Performs an HTTP POST request using the GM_xmlhttpRequest function
* @param {string} url
* @param {Object} [headers = null] default null
* @param {string} [data = null] default null
* @param {boolean} [parseToJSON = true] default true
* @returns {Promise} Promise object, when resolved contains the response text and
* when rejected contains a message or an exception error object
*/
function httpPOSTRequest(url, headers = null, data = null, parseToJSON = true) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: url,
...(headers && {headers: headers}),
...(data && {data: data}),
timeout: 10000,
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")
})
})
}
/**
* Checks if the current location corresponds to a story page
* @returns {boolean} true is it is a story page, false otherwise
*/
function isOnStoryPage() {
return window.location.pathname.startsWith("/stories/")
}
/**
* Checks if the current location corresponds to a single post page
* @returns {boolean} true is it is a story page, false otherwise
*/
function isOnSinglePostPage() {
return window.location.pathname.startsWith("/p/")
}
/**
* Checks if the current location corresponds to a profile page
* @returns {boolean} true is it is a story page, false otherwise
*/
function isOnProfilePage() {
return window.location.pathname.length > 1 &&
document.querySelector(`.${IG_C_PROFILE_CONTAINER}`)
}
/**
* Extracts every URL found between quotes and double quotes in a given string
* Note: This regex is not bullet proof and has unnecessary rules, but it's works fine
* @param {string} string String to match
*/
function extractUrlsFromString(string) {
return string.match(/(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:;,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:;,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:;,.]*\)|[A-Z0-9+&@#\/%=~_|$])/igm)
}
/**
* Extracts every substring found between quotes and double quotes in a given string
* @param {string} string String to match
*/
function extractStringsBetweenQuotes(string) {
return string.match(/(?=["'])(?:"[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*')/gi)
}
/**
* Matches a CSS selector against a DOM element object to check if the element exist in the node
* @param {string} selector
* @param {Object} node DOM element node
* @returns {boolean} true if the element exists in the node, otherwise false
*/
function elementExistsInNode(selector, node) {
return (node.querySelector(selector) != null)
}
/**
* Appends the necessary style elements to DOM using the GM_addStyle function
*/
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))
}
/**
* Shows an alert with an error message and logs an exception to the console
* @param {string} error
* @param {(Object|string)} exception
*/
function showAndLogError(error, exception) {
showMessage(error)
logError(error, exception)
}
/**
* Shows an alert with a message
* @param {string} message
*/
function showMessage(message) {
alert(`Instagram Source Opener:\n${message}`)
}
/**
* Prints a message to the console, either as info or warning
* @param {string} message
* @param {boolean} warning
*/
function logMessage(message, warning = false) {
if (LOGGING_ENABLED && message) {
if (warning) {
console.warn(`[ISO] ${message}`)
} else {
console.info(`[ISO] ${message}`)
}
}
}
/**
* Logs an error string and exception to the console
* @param {string} error
* @param {(Object|string)} exception
*/
function logError(error, exception = null) {
if (LOGGING_ENABLED && error) {
if (exception) {
console.error(`[ISO] ${error}`, exception)
} else {
console.error(`[ISO] ${error}`)
}
}
}
})()