您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Open the original source of an IG post, story or profile picture. No jQuery
当前为
// ==UserScript== // @name Instagram Source Opener // @version 0.9.2 // @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_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @namespace https://gf.qytechs.cn/users/192987 // ==/UserScript== (function() { "use strict" const LOGGING_ENABLED = false /* NOTE: 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})` /* Local storage keys */ const POST_STORY_KB_STORAGE_KEY = "iso_post_story_kb", PROFILE_PICTURE_KB_STORAGE_KEY = "iso_profile_picture_kb" /* Default letters for key bindings */ const DEFAULT_POST_STORY_KB = "O", DEFAULT_PROFILE_PICTURE_KB = "P" /* Global scope variables */ let isStoryKeyBindingSetup = false, isSinglePostKeyBindingSetup = false, isProfileKeyBindingSetup = false let openPostStoryKeyBinding = DEFAULT_POST_STORY_KB, openProfilePictureKeyBinding = DEFAULT_PROFILE_PICTURE_KB /* BEGIN SCRIPT ------------------------------ */ registerMenuCommands() /* 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) setupSinglePostEventListeners() }) /* triggered whenever a story is opened */ document.arrive(`.${IG_C_STORY_CONTAINER}`, node => { generateStoryButton(node) setupStoryEventListeners() }) /* triggered whenever a profile page is loaded */ document.arrive(`.${IG_C_PROFILE_CONTAINER}`, node => { generateProfilePictureButton(node) setupProfileEventListeners() }) /* triggered whenever a single post is closed (on a profile) */ document.leave(`.${IG_C_SINGLE_POST_CONTAINER}`, node => removeSinglePostEventListeners()) /* triggered whenever a story is closed */ document.leave(`.${IG_C_STORY_CONTAINER}`, node => removeStoryEventListeners()) /* triggered whenever a profile page is left */ document.leave(`.${IG_C_PROFILE_CONTAINER}`, node => removeProfileEventListeners()) /** * 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) } setupSinglePostEventListeners() } else if (isOnStoryPage()) { let node = document.querySelector(`.${IG_C_STORY_CONTAINER}`) if (node == null) { generateStoryButton(node) } setupStoryEventListeners() } else if (isOnProfilePage()) { let node = document.querySelector(`.${IG_C_PROFILE_CONTAINER}`) if (node != null) { generateProfilePictureButton(node) } setupProfileEventListeners() } }) /** * Creates the commands to appear on the menu created by the <Any>monkey extension that's being used * For example, on Tampermonkey, this menu is accessible by clicking on the extension icon */ function registerMenuCommands() { try { logMessage("Registered menu commands") GM_registerMenuCommand("Change post/story key binding", handlePostStoryKBMenuCommand, null) GM_registerMenuCommand("Change profile picture key binding", handleProfilePictureKBMenuCommand, null) } catch (error) { logError("Failed to register menu commands", error) } } /** * Handles the click action on the option to change the single post and story opening key binding, on the settings menu */ function handlePostStoryKBMenuCommand() { handleKBMenuCommand(POST_STORY_KB_STORAGE_KEY, DEFAULT_POST_STORY_KB, "single post and story") .then(newKeyBinding => { openPostStoryKeyBinding = newKeyBinding if (isOnSinglePostPage()) { removeSinglePostEventListeners() setupSinglePostEventListeners() } else if (isOnStoryPage()) { removeStoryEventListeners() setupSinglePostEventListeners() } }) } /** * Handles the click action on the option to change the profile picture opening key binding, on the settings menu */ function handleProfilePictureKBMenuCommand() { handleKBMenuCommand(PROFILE_PICTURE_KB_STORAGE_KEY, DEFAULT_PROFILE_PICTURE_KB, "profile picture") .then(newKeyBinding => { openProfilePictureKeyBinding = newKeyBinding removeProfileEventListeners() setupProfileEventListeners() }) } /** * Generic handler for the click action on the key binding changing options of the settings menu. * Launches a prompt that asks the user for a new key binding for a specific action, saves it on local storage and returns it on promise resolve * @param {string} keyBindingStorageKey Local storage key used to store the key binding * @param {string} defaultKeyBinding Default key binding, used on the prompt message * @param {string} keyBindingName Key binding name to show on log messages, just for context * @returns {Promise} Promise object, when resolved contains the new key binding and when rejected contains * an exception error object or nothing when the prompt was canceled or the input was left empty */ function handleKBMenuCommand(keyBindingStorageKey, defaultKeyBinding, keyBindingName) { return new Promise(async (resolve, reject) => { let currentKey = defaultKeyBinding try { currentKey = await GM_getValue(keyBindingStorageKey, defaultKeyBinding) } catch (error) { logError(`Failed to get current "${keyBindingName} key binding, considering default (Alt + ${defaultKeyBinding})`, error) } let newKeyBinding = prompt(`Enter a new letter for the key binding used to open a ${keyBindingName}\n` + `This letter then can be combined with the Alt key to perform said action\n\n` + `Current key binding: Alt + ${(await currentKey).toUpperCase()}`) if (newKeyBinding != null) { if (isKeyBindingValid(newKeyBinding)) { try { await GM_setValue(keyBindingStorageKey, newKeyBinding) showMessage(`Saved new key binding to open ${keyBindingName}: Alt + ${newKeyBinding.toUpperCase()}`) resolve(newKeyBinding) } catch (error) { showAndLogError(`Failed to save new key binding to open ${keyBindingName}`, error) reject(error) } } } }) } /** * 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 */ async function setupSinglePostEventListeners() { if (!isSinglePostKeyBindingSetup) { loadPostStoryKeyBindings() .then(() => { logMessage("Added single post opening key binding") document.addEventListener('keydown', handleSinglePostKeyPress) isSinglePostKeyBindingSetup = true }) } } /** * Adds event listener(s) to the current document meant to handle key presses on a story page */ function setupStoryEventListeners() { if (!isStoryKeyBindingSetup) { loadPostStoryKeyBindings() .then(() => { logMessage("Added story key bindings") document.addEventListener('keydown', handleStoryKeyPress) isStoryKeyBindingSetup = true }) } } /** * Adds event listener(s) to the current document meant to handle key presses on a profile page */ function setupProfileEventListeners() { if (!isProfileKeyBindingSetup) { loadProfilePictureKeyBindings() .then(() => { logMessage("Added profile key bindings") document.addEventListener('keydown', handleProfileKeyPress) isProfileKeyBindingSetup = true }) } } /** * Removes the previously added event listener(s) meant to handle key presses on a single post page */ function removeSinglePostEventListeners() { if (isSinglePostKeyBindingSetup) { logMessage("Removed single post key bindings") document.removeEventListener('keydown', handleSinglePostKeyPress) isSinglePostKeyBindingSetup = false } } /** * Removes the previously added event listener(s) meant to handle key presses on a story page */ function removeStoryEventListeners() { if (isStoryKeyBindingSetup) { logMessage("Removed story key bindings") document.removeEventListener('keydown', handleStoryKeyPress) isStoryKeyBindingSetup = false } } /** * Removes the previously added event listener(s) meant to handle key presses on a profile page */ function removeProfileEventListeners() { if (isProfileKeyBindingSetup) { logMessage("Removed profile key bindings") document.removeEventListener('keydown', handleProfileKeyPress) isProfileKeyBindingSetup = false } } /** * Handles key up events on a story page * @param {Object} e Event object */ function handleStoryKeyPress(e) { if (e.key === openPostStoryKeyBinding.toLowerCase()) { if (isOnStoryPage()) { if (/* alt key was being pressed */ e.altKey) { 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 handleSinglePostKeyPress(e) { if (e.key === openPostStoryKeyBinding.toLowerCase()) { if (isOnSinglePostPage()) { if (/* alt key was being pressed */ e.altKey) { 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 handleProfileKeyPress(e) { if (e.key === openProfilePictureKeyBinding.toLowerCase()) { /* NOTE: Could be possible to open a profile picture while on a single post pop-up, but allowing that can lead to having overlapping key bindings if the user chooses to have the same key binding for opening posts, stories and profile pictures */ if (!isOnStoryPage() && !isOnSinglePostPage()) { if (/* alt key was being pressed */ e.altKey) { logMessage("Detected profile picture opening shortcut on a profile page") openProfilePicture() } } } } /** * Loads the key bind to open a single post or a story from local storage into a global scope variable, in order * to be used on the key binding handler method * @returns {Promise} Promise object, always resolved after loading key binding */ function loadPostStoryKeyBindings() { return new Promise(async resolve => { try { let kbName = "single post and story" openPostStoryKeyBinding = await loadKeyBindingFromStorage( POST_STORY_KB_STORAGE_KEY, DEFAULT_POST_STORY_KB, kbName) } catch (error) { logError(`Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_POST_STORY_KB})`, error) } finally { resolve() } }) } /** * Loads the key bind to open a profile picture from local storage into a global scope variable in order * to be used on the key binding handler method * @returns {Promise} Promise object, always resolved after loading key binding */ function loadProfilePictureKeyBindings() { return new Promise(async resolve => { try { let kbName = "profile picture" openProfilePictureKeyBinding = await loadKeyBindingFromStorage( PROFILE_PICTURE_KB_STORAGE_KEY, DEFAULT_PROFILE_PICTURE_KB, kbName) } catch (error) { logError(`Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_PROFILE_PICTURE_KB})`, error) } finally { resolve() } }) } /** * Loads a key binding from local storage, if it fails or doesn't have anything stores, returns the fallback key binding * @param {string} storageKey Local storage key used to store the key binding * @param {string} defaultKeyBinding Fallback key binding * @param {string} keyBindingName Key binding name to show on log messages, just for context * @returns {Promise} Promise object, when resolved contains the loaded key and when rejected contains an exception error object */ function loadKeyBindingFromStorage(storageKey, defaultKeyBinding, keyBindingName) { return new Promise(async resolve => { try { let kb = await GM_getValue(storageKey, defaultKeyBinding) if (isKeyBindingValid(await kb)) { let newKey = kb.toUpperCase() logMessage(`Loaded "${keyBindingName}" key binding: Alt + ${newKey}`) resolve(newKey) } else { logError(`Couldn't load "${keyBindingName}" key binding, "${kb}" key is invalid, considering default (Alt + ${defaultKeyBinding})`) resolve(defaultKeyBinding) } } catch (error) { logError(`Failed to load "${keyBindingName}" key binding, considering default (Alt + ${defaultKeyBinding})`, error) reject(error) } }) } /** * 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}`) } /** * Check if the key is valid to used as a key binding * @param {string} key Key binding key * @returns {boolean} true is the key is valid, false otherwise */ function isKeyBindingValid(key) { return /[a-zA-Z]/gm.test(key) } /** * 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 */ 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}` let element = document.createElement('style'); element.type = 'text/css'; element.innerHTML = styles; document.head.appendChild(element); } /** * 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}`) } } } })()
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址