Greasy Fork is available in English.

(Deprecated) Instagram Source Opener

Open the original source of an IG post, story or profile picture

  1. // ==UserScript==
  2. // @name (Deprecated) Instagram Source Opener
  3. // @version 1.5.2
  4. // @description Open the original source of an IG post, story or profile picture
  5. // @author jomifepe
  6. // @license MIT
  7. // @icon https://www.instagram.com/favicon.ico
  8. // @match https://www.instagram.com/*
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM.xmlHttpRequest
  11. // @grant GM_registerMenuCommand
  12. // @grant GM.registerMenuCommand
  13. // @grant GM_getValue
  14. // @grant GM.getValue
  15. // @grant GM_setValue
  16. // @grant GM.setValue
  17. // @grant GM_deleteValue
  18. // @grant GM.deleteValue
  19. // @grant GM_openInTab
  20. // @grant GM.openInTab
  21. // @connect instagram.com
  22. // @connect i.instagram.com
  23. // @namespace https://jomifepe.github.io/
  24. // @supportURL https://github.com/jomifepe/userscripts/issues
  25. // @homepage https://github.com/jomifepe/userscripts/tree/main/instagram-source-opener
  26. // @contributionURL https://www.paypal.com/donate?hosted_button_id=JT2G5E5SM9C88
  27. // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
  28. // ==/UserScript==
  29.  
  30. /* jshint esversion: 10 */
  31. (async function () {
  32. 'use strict';
  33.  
  34. /* eslint-disable no-unused-vars */
  35.  
  36. const SCRIPT_NAME = 'Instagram Source Opener',
  37. SCRIPT_NAME_SHORT = 'ISO',
  38. HOMEPAGE_URL = 'https://greasyfork.org/en/scripts/372366-instagram-source-opener',
  39. SESSION_ID_INFO_URL = 'https://greasyfork.org/en/scripts/372366-instagram-source-opener#sessionid',
  40. USER_AGENT =
  41. 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 105.0.0.11.118 (iPhone11,8; iOS 12_3_1; en_US; en-US; scale=2.00; 828x1792; 165586599)',
  42. IG_APP_ID = '936619743392459';
  43.  
  44. /* Instagram classes and selectors */
  45. const IG_S_STORY_CONTAINER = '.yS4wN,.vUg3G,.yUdUG,._a997._ac6a._ac0e',
  46. IG_S_SINGLE_POST_CONTAINER = '.JyscU,.PdwC2,article[role="presentation"]',
  47. IG_S_POST_IMAGE_CONTAINER = `${IG_S_SINGLE_POST_CONTAINER} > div:first-child`,
  48. IG_S_PROFILE_CONTAINER = '.v9tJq,.XjzKX,main._a993',
  49. IG_S_STORY_MEDIA_CONTAINER = '.qbCDp,._ab8w._ab94._ab97._ab9f._ab9k._ab9p._abcm',
  50. IG_S_POST_IMG = `.FFVAD,${IG_S_SINGLE_POST_CONTAINER} ._aagv img`,
  51. IG_S_POST_VIDEO = `.tWeCl,${IG_S_SINGLE_POST_CONTAINER} ._ab1c video`,
  52. IG_S_POST_BUTTONS = `.eo2As > section,${IG_S_SINGLE_POST_CONTAINER} section`,
  53. IG_S_PROFILE_PIC_CONTAINER = `.RR-M-,${IG_S_PROFILE_CONTAINER} header > div:first-child > div:first-child`,
  54. IG_S_PRIVATE_PROFILE_PIC_CONTAINER = '._4LQNo',
  55. IG_S_PROFILE_USERNAME_TITLE = '.fKFbl,h2',
  56. IG_S_POST_BLOCKER = '._9AhH0',
  57. IG_S_TOP_BAR = '.Hz2lF,._lz6s,nav._acbh._acbi',
  58. IG_S_POST_TIME_ELEMENT = `.c-Yi7,${IG_S_SINGLE_POST_CONTAINER} time._aaqe`,
  59. IG_S_MULTI_VERTICAL_POST_INDICATOR = '.Yi5aA,._aamk._acvz._acnc._acne > *',
  60. IG_S_MULTI_HORIZONTAL_POST_INDICATOR = '.Yi5aA,._aamj._acvz._acnc._acng > *',
  61. IG_S_PROFILE_PRIVATE_MESSAGE = '.rkEop',
  62. IG_S_PROFILE_HAS_STORIES_INDICATOR = 'header [aria-disabled=false] canvas';
  63.  
  64. /* Custom classes and selectors */
  65. const C_BTN_STORY = 'iso-story-btn',
  66. C_POST_WITH_BUTTON = 'iso-post',
  67. C_BTN_POST = 'iso-post-btn',
  68. C_PROFILE_BUTTON_CONTAINER = 'iso-profile-button-container',
  69. C_BTN_PROFILE_PIC = 'iso-profile-picture-btn',
  70. C_BTN_ANONYMOUS_STORIES = 'iso-anonymous-stories-btn',
  71. /* Base modal classes */
  72. C_MODAL_BACKDROP = 'iso-modal-backdrop',
  73. C_MODAL_WRAPPER = 'iso-modal-wrapper',
  74. C_MODAL_CLOSE_BTN = 'iso-modal-close-btn',
  75. /* Script settings */
  76. C_SETTINGS_BTN = 'iso-settings-btn',
  77. C_SETTINGS_MODAL = 'iso-settings-modal',
  78. C_SETTINGS_SECTION_COLLAPSED = 'iso-settings-section-collapsed',
  79. ID_SETTINGS_POST_STORY_KB_BTN = 'iso-settings-post-story-kb-btn',
  80. ID_SETTINGS_PROFILE_PICTURE_KB_BTN = 'iso-settings-profile-picture-kb-btn',
  81. ID_SETTINGS_BUTTON_BEHAVIOR_SELECT = 'iso-settings-button-behavior-select',
  82. ID_SETTINGS_DEVELOPER_OPTIONS_BTN = 'iso-settings-developer-options-btn',
  83. ID_SETTINGS_DEVELOPER_OPTIONS_CONTAINER = 'iso-settings-developer-options-container',
  84. ID_SETTINGS_SESSION_ID_INPUT = 'iso-settings-session-id-input',
  85. ID_SETTINGS_DEBUGGING_INPUT = 'iso-settings-debugging-checkbox',
  86. ID_SETTINGS_COPY_DEBUG_LOGS = 'iso-settings-copy-debug-logs',
  87. S_IG_POST_CONTAINER_WITHOUT_BUTTON = `${IG_S_SINGLE_POST_CONTAINER}:not(.${C_POST_WITH_BUTTON})`,
  88. /* Anonymous stories modal */
  89. C_STORIES_MODAL = 'iso-stories-modal',
  90. C_STORIES_MODAL_LIST = 'iso-stories-modal-list',
  91. C_STORIES_MODAL_LIST_ITEM = 'iso-stories-modal-list-item';
  92.  
  93. /* Storage and cookie keys */
  94. const STORAGE_KEY_POST_STORY_KB = 'iso_post_story_kb',
  95. STORAGE_KEY_PROFILE_PICTURE_KB = 'iso_profile_picture_kb',
  96. STORAGE_KEY_BUTTON_BEHAVIOR = 'iso_button_behavior',
  97. STORAGE_KEY_SESSION_ID = 'iso_session_id',
  98. STORAGE_KEY_DEBUGGING_ENABLED = 'iso_debugging_enabled',
  99. COOKIE_IG_USER_ID = 'ds_user_id',
  100. /* Default letters for key bindings */
  101. DEFAULT_KB_POST_STORY = 'O',
  102. DEFAULT_KB_PROFILE_PICTURE = 'P',
  103. /* Open source button behavior keys */
  104. BUTTON_BEHAVIOR_REDIR = 'bb_redirect',
  105. BUTTON_BEHAVIOR_NEW_TAB_FOCUS = 'bb_tab_focus',
  106. BUTTON_BEHAVIOR_NEW_TAB_BG = 'bb_tab_background',
  107. DEFAULT_BUTTON_BEHAVIOR = BUTTON_BEHAVIOR_NEW_TAB_FOCUS,
  108. BUTTON_BEHAVIOR_OPTIONS = [BUTTON_BEHAVIOR_REDIR, BUTTON_BEHAVIOR_NEW_TAB_FOCUS, BUTTON_BEHAVIOR_NEW_TAB_BG];
  109.  
  110. const PATTERN = {
  111. URL_PATH_PARTS: /\/([a-zA-Z0-9._]{0,})/,
  112. IG_VALID_USERNAME: /^([a-zA-Z0-9._]{0,30})$/,
  113. COOKIE_VALUE: (key) => new RegExp(`(^| )${key}=([^;]+)`),
  114. PAGE_SINGLE_MEDIA: /^\/(p|reel|tv)\//,
  115. PAGE_STORIES: /^\/stories\//,
  116. /** matches: `/user`, `/user/tagged`, `/user/reels`, or `/user/channel` */
  117. PAGE_PROFILE: /^\/(([^/]*)\/$|([^/]*)\/(tagged|reels|channel))/,
  118. };
  119.  
  120. const API = {
  121. /** @type {(postOrUsernamePath: string) => string} */
  122. IG_INFO_API: (postOrUsernamePath) => `https://www.instagram.com${postOrUsernamePath}?__a=1&__d=1`,
  123. /** @type {(mediaId: string) => string} */
  124. IG_MEDIA_INFO_API: (mediaId) => `https://i.instagram.com/api/v1/media/${mediaId}/info/`,
  125. /** @type {() => string} */
  126. IG__A1_CURRENT_PAGE: () => `${window.location.href}?__a=1&__d=1`,
  127. /** @type {(userId: string) => string} */
  128. IG_USER_INFO_API: (userId) => `https://i.instagram.com/api/v1/users/${userId}/info/`,
  129. /** @type {(userId: string) => string} */
  130. IG_REELS_FEED_API: (userId) => `https://i.instagram.com/api/v1/feed/reels_media?reel_ids=${userId}`,
  131. };
  132.  
  133. const Logger = createLogger(SCRIPT_NAME_SHORT);
  134.  
  135. const cachedApiData = {
  136. userBasicInfo: buildCache(),
  137. userInfo: buildCache(),
  138. userStories: buildCache(),
  139. userProfilePicture: buildCache(),
  140. post: buildCache(),
  141. };
  142.  
  143. let LOGGING_ENABLED = /** @type boolean */ (await callGMFunction('getValue', STORAGE_KEY_DEBUGGING_ENABLED, false));
  144.  
  145. let isStoryKeyBindingSetup, isSinglePostKeyBindingSetup, isProfileKeyBindingSetup;
  146. let openPostStoryKeyBinding = DEFAULT_KB_POST_STORY;
  147. let openProfilePictureKeyBinding = DEFAULT_KB_PROFILE_PICTURE;
  148. let openSourceBehavior = '';
  149. let sessionId = '';
  150.  
  151. const pages = {
  152. feed: {
  153. isVisible: () => window.location.pathname === '/',
  154. onLoadActions: () => {
  155. qsa(document, S_IG_POST_CONTAINER_WITHOUT_BUTTON).forEach(generatePostButtons);
  156. },
  157. },
  158. story: {
  159. isVisible: () => PATTERN.PAGE_STORIES.test(window.location.pathname),
  160. onLoadActions: () => {
  161. generateStoryButton();
  162. setupStoryEventListeners();
  163. },
  164. },
  165. profile: {
  166. isVisible: () => PATTERN.PAGE_PROFILE.test(window.location.pathname),
  167. onLoadActions: () => {
  168. if (!checkIsLoggedIn()) return;
  169. const node = qs(document, IG_S_PROFILE_CONTAINER);
  170. if (!node) return;
  171. generateProfileElements();
  172. setupProfileEventListeners();
  173. },
  174. },
  175. post: {
  176. isVisible: () => PATTERN.PAGE_SINGLE_MEDIA.test(window.location.pathname),
  177. onLoadActions: () => {
  178. const node = qs(document, IG_S_SINGLE_POST_CONTAINER);
  179. if (!node) return;
  180. generatePostButtons(node);
  181. setupSinglePostEventListeners();
  182. },
  183. },
  184. };
  185.  
  186. const actionTriggers = {
  187. arrive: {
  188. /* triggered whenever a new instagram post is loaded on the feed */
  189. [S_IG_POST_CONTAINER_WITHOUT_BUTTON]: (node) => {
  190. if (!pages.post.isVisible() && !pages.feed.isVisible()) return;
  191. generatePostButtons(node);
  192. },
  193. /* triggered whenever a single post is opened (on a profile) */
  194. [IG_S_SINGLE_POST_CONTAINER]: (node) => {
  195. if (!pages.post.isVisible() && !pages.feed.isVisible()) return;
  196. generatePostButtons(node);
  197. setupSinglePostEventListeners();
  198. },
  199. /* triggered whenever a story is opened */
  200. [IG_S_STORY_CONTAINER]: (node) => {
  201. if (!pages.story.isVisible()) return;
  202. generateStoryButton(node);
  203. setupStoryEventListeners();
  204. },
  205. /* triggered whenever a profile page is loaded */
  206. [IG_S_PROFILE_CONTAINER]: (node) => {
  207. if (!pages.profile.isVisible()) return;
  208. generateProfileElements(node);
  209. setupProfileEventListeners();
  210. },
  211. /* triggered whenever the top bar is created */
  212. [IG_S_TOP_BAR]: generateSettingsPageMenu,
  213. },
  214. leave: {
  215. /* triggered whenever a single post is closed (on a profile) */
  216. [IG_S_SINGLE_POST_CONTAINER]: removeSinglePostEventListeners,
  217. /* triggered whenever a story is closed */
  218. [IG_S_STORY_CONTAINER]: removeStoryEventListeners,
  219. /* triggered whenever a profile page is left */
  220. [IG_S_PROFILE_CONTAINER]: removeProfileEventListeners,
  221. },
  222. };
  223.  
  224. registerMenuCommands(); /* register GM menu commands */
  225. injectStyles(); /* injects the needed CSS into DOM */
  226. setupTriggers(); /* setup arrive and leave triggers for elements */
  227. performOnLoadActions(); /* first load actions */
  228. window.onload = performOnLoadActions; /* first load actions (backup) */
  229.  
  230. /** Setup the arrive and leave triggers for relevant elements */
  231. function setupTriggers() {
  232. let count = 0;
  233. for (const [event, triggers] of Object.entries(actionTriggers)) {
  234. for (const [actuator, fireTrigger] of Object.entries(triggers)) {
  235. document[event](actuator, (node) => {
  236. if (!node) return;
  237. fireTrigger(node);
  238. Logger.log(`Triggered ${event} for selector ${actuator}`);
  239. });
  240. count++;
  241. }
  242. }
  243. Logger.log(`Created ${count} element triggers`);
  244. }
  245.  
  246. /**
  247. * Performs actions that need to be performed on page load.
  248. */
  249. function performOnLoadActions() {
  250. for (const [name, page] of Object.entries(pages)) {
  251. if (page.isVisible()) {
  252. page.onLoadActions();
  253. Logger.log(`Performed onload actions for ${name} page`);
  254. }
  255. }
  256. loadPreferences();
  257. generateSettingsPageMenu();
  258. }
  259.  
  260. /**
  261. * Loads preferences that are needed on multiple occasions from the storage to the corresponding variables
  262. */
  263. async function loadPreferences() {
  264. if (!openSourceBehavior) {
  265. const savedOsb = await callGMFunction('getValue', STORAGE_KEY_BUTTON_BEHAVIOR, undefined);
  266. if (!savedOsb) {
  267. Logger.log('Loaded default button behavior');
  268. openSourceBehavior = DEFAULT_BUTTON_BEHAVIOR;
  269. } else if (BUTTON_BEHAVIOR_OPTIONS.includes(savedOsb)) {
  270. openSourceBehavior = savedOsb;
  271. Logger.log('[Loaded preference] Open button behavior:', savedOsb);
  272. }
  273. }
  274.  
  275. if (!sessionId) {
  276. const savedSID = await callGMFunction('getValue', STORAGE_KEY_SESSION_ID, null);
  277. if (!savedSID) {
  278. Logger.log('No saved session id found');
  279. } else {
  280. sessionId = savedSID;
  281. Logger.log(`[Loaded preference] Session id: ...${getLast4Digits(savedSID)}`);
  282. }
  283. }
  284. }
  285.  
  286. /**
  287. * Creates the commands to appear on the menu created by the <Any>monkey extension that's being used
  288. * For example, on Tampermonkey, this menu is accessible by clicking on the extension icon
  289. */
  290. function registerMenuCommands() {
  291. callGMFunction('registerMenuCommand', 'Change post & story shortcut', handleMenuPostStoryKBCommand);
  292. callGMFunction('registerMenuCommand', 'Change profile picture shortcut', handleMenuProfilePicKBCommand);
  293. Logger.log('Registered menu commands');
  294. }
  295.  
  296. /**
  297. * Handle the click on the settings menu option to change the single post and story opening key binding
  298. */
  299. async function handleMenuPostStoryKBCommand() {
  300. const kb = await handleKBMenuCommand(STORAGE_KEY_POST_STORY_KB, DEFAULT_KB_POST_STORY, 'single post and story');
  301. if (!kb) return;
  302. openPostStoryKeyBinding = kb;
  303. if (pages.post.isVisible()) {
  304. removeSinglePostEventListeners();
  305. setupSinglePostEventListeners();
  306. return;
  307. }
  308. if (pages.story.isVisible()) {
  309. removeStoryEventListeners();
  310. setupStoryEventListeners();
  311. return;
  312. }
  313. }
  314.  
  315. /**
  316. * Handle the click on the settings menu option to change the profile picture opening key binding
  317. */
  318. async function handleMenuProfilePicKBCommand() {
  319. const kb = await handleKBMenuCommand(STORAGE_KEY_PROFILE_PICTURE_KB, DEFAULT_KB_PROFILE_PICTURE, 'profile picture');
  320. if (!kb) return;
  321. openProfilePictureKeyBinding = kb;
  322. removeProfileEventListeners();
  323. setupProfileEventListeners();
  324. }
  325.  
  326. /**
  327. * Handle the click on the settings menu option to change the profile picture opening key binding
  328. * @param {InputEvent} event Input change event
  329. */
  330. async function handleMenuButtonBehaviorChange(event) {
  331. const option = /** @type string */ event.target.value;
  332. if (!BUTTON_BEHAVIOR_OPTIONS.includes(option)) {
  333. Logger.error('Invalid option for source button behavior');
  334. return;
  335. }
  336. const result = await callGMFunction('setValue', STORAGE_KEY_BUTTON_BEHAVIOR, option);
  337. if (result === null) Logger.error('Failed to save button behavior option on storage');
  338. openSourceBehavior = option;
  339. Logger.log('Changed open source button behavior to', option);
  340. }
  341.  
  342. /**
  343. * Handle a new sessionid entered on the developer options section of the settings menu
  344. * @param {InputEvent} event Input change event
  345. */
  346. async function handleSessionIdChange(event) {
  347. const value = /** @type string */ (event.target.value);
  348. const newSessionId = value?.trim();
  349. if (value === null || typeof myVar !== 'undefined' || newSessionId === sessionId) return; // empty values are accepted
  350. if (newSessionId.length === 0 && sessionId) {
  351. await callGMFunction('deleteValue', STORAGE_KEY_SESSION_ID);
  352. sessionId = '';
  353. Logger.log('Deleted saved session id');
  354. return;
  355. }
  356. const result = await callGMFunction('setValue', STORAGE_KEY_SESSION_ID, newSessionId);
  357. if (result === null) Logger.error('Failed to save session id in storage');
  358. sessionId = newSessionId;
  359. Logger.log(`Saved current session id: ...${getLast4Digits(newSessionId)}`);
  360. }
  361.  
  362. /**
  363. * Handle 'debugging enabled' checkbox change events
  364. * @param {InputEvent} event Input change event
  365. */
  366. async function handleDebuggingSettingChange(event) {
  367. try {
  368. const enabled = /** @type boolean */ (event.target.checked);
  369. await callGMFunction('setValue', STORAGE_KEY_DEBUGGING_ENABLED, enabled);
  370. Logger.force.log(`${enabled ? 'Enabled' : 'Disabled'} debugging`);
  371. LOGGING_ENABLED = enabled;
  372. } catch (error) {
  373. Logger.force.error('Failed to store debugging enabled in storage', error);
  374. }
  375. }
  376.  
  377. /** Handle 'copy debug logs' button click */
  378. async function handleCopyDebugLogs() {
  379. try {
  380. await navigator.clipboard.writeText(Logger.logs.join('\n'));
  381. Logger.alert('Coppied to clipboard');
  382. } catch (error) {
  383. const message = 'Failed to copy debug logs to clipboard';
  384. Logger.error(message, error);
  385. Logger.alert(message);
  386. }
  387. }
  388.  
  389. /**
  390. * Generic handler for the click action on the key binding changing options of the settings menu.
  391. * Launches a prompt that asks the user for a new key binding for a specific action, saves it locally and returns it
  392. * @param {string} keyBindingStorageKey Unique name used to store the key binding
  393. * @param {string} defaultKeyBinding Default key binding, used on the prompt message
  394. * @param {string} keyBindingName Key binding name to show on log messages, just for context
  395. * @returns {Promise<string|null>} Promise object, returns either the new key binding or null if it failed
  396. */
  397. async function handleKBMenuCommand(keyBindingStorageKey, defaultKeyBinding, keyBindingName) {
  398. let currentKey = await callGMFunction('getValue', keyBindingStorageKey, defaultKeyBinding);
  399. if (currentKey == null) {
  400. currentKey = defaultKeyBinding;
  401. Logger.log(`Falling back to default key binding: Alt + ${defaultKeyBinding}`);
  402. }
  403.  
  404. const newKeyBinding = prompt(
  405. `${SCRIPT_NAME}:\n\nKey binding to open a ${keyBindingName}:\n` +
  406. 'Choose a letter to be combined with the Alt/⌥ key\n\n' +
  407. `Current key binding: Alt/⌥ + ${currentKey.toUpperCase()}`
  408. );
  409. if (newKeyBinding == null) return null;
  410. if (!isKeyBindingValid(newKeyBinding)) {
  411. Logger.alertAndLog(`Couldn't save new key binding to open ${keyBindingName}, invalid option`);
  412. return null;
  413. }
  414.  
  415. const successMessage = `Saved new shortcut to open ${keyBindingName}:\nAlt + ${newKeyBinding.toUpperCase()}`;
  416. const result = await callGMFunction('setValue', keyBindingStorageKey, newKeyBinding);
  417. if (result === null) return null;
  418. Logger.alert(successMessage);
  419. return newKeyBinding;
  420. }
  421.  
  422. /**
  423. * Changes the visibility of the page settings menu
  424. * @param {boolean} visible
  425. */
  426. function setSettingsMenuVisible(visible) {
  427. if (visible) {
  428. qs(document, `.${C_SETTINGS_MODAL}`).style.setProperty('display', 'flex', 'important');
  429.  
  430. /* load values on the menu */
  431. const buttonBehaviorSelect = qs(document, `#${ID_SETTINGS_BUTTON_BEHAVIOR_SELECT}`);
  432. if (buttonBehaviorSelect) buttonBehaviorSelect.value = openSourceBehavior;
  433. const sessionIdInput = qs(document, `#${ID_SETTINGS_SESSION_ID_INPUT}`);
  434. if (sessionIdInput) sessionIdInput.value = sessionId;
  435. const debuggingEnabledInput = qs(document, `#${ID_SETTINGS_DEBUGGING_INPUT}`);
  436. if (debuggingEnabledInput) debuggingEnabledInput.checked = LOGGING_ENABLED;
  437. } else {
  438. qs(document, `.${C_SETTINGS_MODAL}`).style.setProperty('display', 'none', 'important');
  439. }
  440. }
  441.  
  442. /**
  443. * Toggles the visibility of the settings menu
  444. * @param {boolean} visible
  445. */
  446. function setAnonymousStoriesModalVisible(visible) {
  447. const value = visible ? 'flex' : 'none';
  448. qs(document, `.${C_STORIES_MODAL}`).style.setProperty('display', value, 'important');
  449. }
  450.  
  451. /**
  452. * Creates a visual settings menu on the page, as an alternative to the commands menu method,
  453. * since it isn't supported by all extensions
  454. */
  455. function generateSettingsPageMenu() {
  456. if (!qs(document, `.${C_SETTINGS_BTN}`)) {
  457. /* Create the settings button */
  458. const button = createElementFromHtml(`
  459. <button class="${C_SETTINGS_BTN}" type="button" title="Open ${SCRIPT_NAME} settings" />
  460. `);
  461. button.addEventListener('click', () => setSettingsMenuVisible(true));
  462. qs(document, IG_S_TOP_BAR)?.appendChild(button);
  463. Logger.log('Created settings button');
  464. }
  465.  
  466. if (!qs(document, `.${C_SETTINGS_MODAL}`)) {
  467. /* Create the settings menu */
  468. const modal = createElementFromHtml(`
  469. <div class="${C_MODAL_BACKDROP} ${C_SETTINGS_MODAL}"><div class="${C_MODAL_WRAPPER}"><div class="iso-modal-title-container"><div class="iso-modal-title">${SCRIPT_NAME_SHORT} Settings <a class="iso-modal-title-link" href="${HOMEPAGE_URL}" target="_blank" title="What's this?">(?)</a></div><button class="${C_MODAL_CLOSE_BTN}" title="Close"><div class="coreSpriteClose"></div></button></div><div class="iso-modal-content-container"><div class="iso-settings-content-section"><button id="${ID_SETTINGS_POST_STORY_KB_BTN}" class="iso-settings-menu-option-button">Change post/story shortcut</button> <button id="${ID_SETTINGS_PROFILE_PICTURE_KB_BTN}" class="iso-settings-menu-option-button">Change profile picture shortcut</button><div class="iso-flex-column iso-settings-option-container"><label for="${ID_SETTINGS_BUTTON_BEHAVIOR_SELECT}">Open source click behavior:</label> <select id="${ID_SETTINGS_BUTTON_BEHAVIOR_SELECT}"><option value="${BUTTON_BEHAVIOR_REDIR}">Redirect</option><option value="${BUTTON_BEHAVIOR_NEW_TAB_FOCUS}">New tab and focus</option><option value="${BUTTON_BEHAVIOR_NEW_TAB_BG}">New tab in the background</option></select></div><div id="${ID_SETTINGS_DEVELOPER_OPTIONS_BTN}" class="iso-settings-menu-option-button ${C_SETTINGS_SECTION_COLLAPSED}">Developer options <span class="iso-settings-select-arrow"></span></div></div><div id="${ID_SETTINGS_DEVELOPER_OPTIONS_CONTAINER}" class="iso-settings-content-section ${C_SETTINGS_SECTION_COLLAPSED}"><div class="iso-flex-column iso-settings-option-container"><label for="${ID_SETTINGS_SESSION_ID_INPUT}">Session ID <a class="iso-modal-title-link" href="${SESSION_ID_INFO_URL}" target="_blank" title="What's this?">(?)</a></label> <input id="${ID_SETTINGS_SESSION_ID_INPUT}" type="text" placeholder="Your current session id"></div><div class="iso-flex-row-center iso-settings-option-container"><input id="${ID_SETTINGS_DEBUGGING_INPUT}" type="checkbox"> <label for="${ID_SETTINGS_DEBUGGING_INPUT}">Debugging enabled</label></div><button id="${ID_SETTINGS_COPY_DEBUG_LOGS}" class="iso-settings-menu-option-button">Copy debugging logs</button></div></div></div></div>
  470. `);
  471.  
  472. /* handle modal backdrop click */
  473. modal.addEventListener('click', (event) => {
  474. if (!isModalBackdrop(event)) return;
  475. setSettingsMenuVisible(false);
  476. });
  477. /* ignore clicks inside the modal content */
  478. qsael(modal, `.${C_SETTINGS_MODAL} .${C_MODAL_WRAPPER}`, 'click', withStopPropagation);
  479. /* handle menu close on close button click */
  480. qsael(
  481. modal,
  482. `.${C_SETTINGS_MODAL} .${C_MODAL_CLOSE_BTN}`,
  483. 'click',
  484. withPreventDefault(() => setSettingsMenuVisible(false))
  485. );
  486. /* handle post/story key binding button */
  487. qsael(modal, `#${ID_SETTINGS_POST_STORY_KB_BTN}`, 'click', withPreventDefault(handleMenuPostStoryKBCommand));
  488. /* handle profile picture key binding button */
  489. qsael(
  490. modal,
  491. `#${ID_SETTINGS_PROFILE_PICTURE_KB_BTN}`,
  492. 'click',
  493. withPreventDefault(handleMenuProfilePicKBCommand)
  494. );
  495. /* handle change of button behavior option select */
  496. qsael(
  497. modal,
  498. `#${ID_SETTINGS_BUTTON_BEHAVIOR_SELECT}`,
  499. 'change',
  500. withPreventDefault(handleMenuButtonBehaviorChange)
  501. );
  502. /* handle click of developer settings button (toggle view) */
  503. qsael(
  504. modal,
  505. `#${ID_SETTINGS_DEVELOPER_OPTIONS_BTN}`,
  506. 'click',
  507. withPreventDefault(() => {
  508. qs(modal, `#${ID_SETTINGS_DEVELOPER_OPTIONS_BTN}`)?.classList.toggle(C_SETTINGS_SECTION_COLLAPSED);
  509. qs(modal, `#${ID_SETTINGS_DEVELOPER_OPTIONS_CONTAINER}`)?.classList.toggle(C_SETTINGS_SECTION_COLLAPSED);
  510. })
  511. );
  512. /* handle blur of the session id input */
  513. qsael(modal, `#${ID_SETTINGS_SESSION_ID_INPUT}`, 'blur', withPreventDefault(handleSessionIdChange));
  514. /* handle change of the debugging enabled checkbox */
  515. qsael(modal, `#${ID_SETTINGS_DEBUGGING_INPUT}`, 'change', handleDebuggingSettingChange);
  516. qsael(modal, `#${ID_SETTINGS_COPY_DEBUG_LOGS}`, 'click', handleCopyDebugLogs);
  517.  
  518. document.body.appendChild(modal);
  519. Logger.log('Created settings menu');
  520. }
  521. }
  522.  
  523. /**
  524. * Appends new elements to DOM containing the story source opening button
  525. * @param {HTMLElement} node DOM element node
  526. */
  527. function generateStoryButton(node = document.body) {
  528. /* exits if the story button already exists */
  529. if (!node || elementExistsInNode(`.${C_BTN_STORY}`, document)) return;
  530.  
  531. try {
  532. const openButton = createElementFromHtml(`
  533. <button class="${C_BTN_STORY}" type="button" title="Open source" />
  534. `);
  535. openButton.addEventListener('click', () => openStoryContent(node));
  536. node.appendChild(openButton);
  537. } catch (error) {
  538. Logger.error('Failed to generate story button', error);
  539. }
  540. }
  541.  
  542. /**
  543. * Appends new elements to DOM containing the post source opening button
  544. * @param {HTMLElement} node DOM element node
  545. */
  546. function generatePostButtons(node) {
  547. /* exits if the post button already exists */
  548. if (!node || elementExistsInNode(`.${C_BTN_POST}`, node)) return;
  549.  
  550. try {
  551. /* removes the div that's blocking the img element on a post */
  552. const blocker = qs(node, IG_S_POST_BLOCKER);
  553. if (blocker) blocker.parentNode.removeChild(blocker);
  554.  
  555. const postButtonsContainer = qs(node, IG_S_POST_BUTTONS);
  556. if (!postButtonsContainer) {
  557. Logger.error(`Failed to generate post button, couldn't find post buttons container (${IG_S_POST_BUTTONS})`);
  558. return;
  559. }
  560.  
  561. const sourceButton = createElementFromHtml(`
  562. <button class="${C_BTN_POST}" title="Open source" />
  563. `);
  564. sourceButton.addEventListener('click', () => openPostSource(node));
  565. postButtonsContainer.appendChild(sourceButton);
  566. node.classList.add(C_POST_WITH_BUTTON);
  567.  
  568. const timeElement = qs(node, IG_S_POST_TIME_ELEMENT);
  569. if (timeElement) {
  570. const fullDateTime = timeElement.getAttribute('datetime');
  571. const localeDateTime = fullDateTime && new Date(fullDateTime)?.toLocaleString();
  572. if (localeDateTime) timeElement.innerHTML += ` (${localeDateTime})`;
  573. }
  574. } catch (error) {
  575. Logger.error('Failed to generate post button', error);
  576. }
  577. }
  578.  
  579. /**
  580. * Appends new elements to DOM containing the profile picture source opening button
  581. * @param {HTMLElement} node DOM element node
  582. */
  583. function generateProfileElements(node = document) {
  584. const profilePicContainer = qs(node, IG_S_PROFILE_PIC_CONTAINER) || qs(node, IG_S_PRIVATE_PROFILE_PIC_CONTAINER);
  585.  
  586. if (profilePicContainer) {
  587. const buttonContainer = createElementFromHtml(`<div class="${C_PROFILE_BUTTON_CONTAINER}"></div>`);
  588. profilePicContainer.appendChild(buttonContainer);
  589.  
  590. /* generate the profile picture source button */
  591. try {
  592. if (!elementExistsInNode(`.${C_BTN_PROFILE_PIC}`, node)) {
  593. const profilePictureButton = createElementFromHtml(`
  594. <button class="${C_BTN_PROFILE_PIC}" title="Open full size profile picture" />
  595. `);
  596. profilePictureButton.addEventListener('click', withStopPropagation(openProfilePicture));
  597. buttonContainer.appendChild(profilePictureButton);
  598. Logger.log('Generated profile picture button');
  599. }
  600. } catch (error) {
  601. Logger.error('Failed to generate picture button', error);
  602. }
  603.  
  604. /* generate the anonymous story button */
  605. try {
  606. const hasStories = !!qs(document, IG_S_PROFILE_HAS_STORIES_INDICATOR);
  607. if (!elementExistsInNode(`.${C_BTN_ANONYMOUS_STORIES}`, node) && hasStories) {
  608. // if the profile is not private or you follow the user
  609. if (!qs(document, IG_S_PROFILE_PRIVATE_MESSAGE)) {
  610. const storiesButton = createElementFromHtml(`
  611. <button class="${C_BTN_ANONYMOUS_STORIES}" title="View user stories anonymously" />
  612. `);
  613. storiesButton.addEventListener('click', withStopPropagation(openAnonymousStoriesModal));
  614. buttonContainer.appendChild(storiesButton);
  615. Logger.log('Generated anonymous stories button');
  616. }
  617. }
  618. } catch (error) {
  619. Logger.error('Failed to generate anonymous stories button', error);
  620. }
  621. } else {
  622. Logger.error(`Couldn't find profile picture container (${IG_S_PROFILE_PIC_CONTAINER})`);
  623. }
  624.  
  625. /* generate the anonymous stories modal */
  626. try {
  627. if (!elementExistsInNode(`.${C_STORIES_MODAL}`, node)) {
  628. const modal = createElementFromHtml(`
  629. <div class="${C_MODAL_BACKDROP} ${C_STORIES_MODAL}"><div class="${C_MODAL_WRAPPER}"><div class="iso-modal-title-container"><div class="iso-modal-title">User Stories (Anonymous) <a class="iso-modal-title-link" href="${HOMEPAGE_URL}" target="_blank" title="What's this?">(?)</a></div><button class="${C_MODAL_CLOSE_BTN}" title="Close"><div class="coreSpriteClose"></div></button></div><div class="iso-modal-content-container"><div class="${C_STORIES_MODAL_LIST}"></div></div></div></div>
  630. `);
  631.  
  632. /* handle modal backdrop click */
  633. modal.addEventListener('click', (event) => {
  634. if (!isModalBackdrop(event)) return;
  635. setAnonymousStoriesModalVisible(false);
  636. });
  637. /* handle menu close on close button click */
  638. qsael(modal, `.${C_STORIES_MODAL} .${C_MODAL_CLOSE_BTN}`, 'click', () =>
  639. setAnonymousStoriesModalVisible(false)
  640. );
  641. document.body.appendChild(modal);
  642. Logger.log('Generated anonymous stories modal');
  643. }
  644. } catch (error) {
  645. Logger.error('Failed to generate anonymous stories modal', error);
  646. }
  647. }
  648.  
  649. function isModalBackdrop(event) {
  650. return event.target.classList.contains(C_MODAL_BACKDROP);
  651. }
  652.  
  653. /** Finds the user's stories and displays them in the modal */
  654. async function openAnonymousStoriesModal() {
  655. try {
  656. if (qs(document, IG_S_PROFILE_PRIVATE_MESSAGE)) {
  657. Logger.alert('You cannot view stories of a private user');
  658. return;
  659. }
  660. if (!qs(document, IG_S_PROFILE_HAS_STORIES_INDICATOR)) {
  661. Logger.alert('This user has no stories at the moment');
  662. return;
  663. }
  664. document.body.style.cursor = 'wait';
  665. const stories = await getUserStories(getProfileUsername());
  666. const listContainer = qs(document, `.${C_STORIES_MODAL_LIST}`);
  667. const storyCardsHtmlArray = stories?.map(
  668. (storyImage) => `
  669. <a
  670. class="${C_STORIES_MODAL_LIST_ITEM}"
  671. href="${storyImage.url}"
  672. target="_blank"
  673. >
  674. <img src="${storyImage.thumbnailUrl}" />
  675. <time datetime="${storyImage.dateTime}">${storyImage.relativeTime}</time>
  676. </a>
  677. `
  678. );
  679. if (!storyCardsHtmlArray) return;
  680.  
  681. listContainer.innerHTML = storyCardsHtmlArray.join('');
  682. setAnonymousStoriesModalVisible(true);
  683. } catch (error) {
  684. const message = 'Failed to get user stories';
  685. Logger.error(message, error);
  686. Logger.alert(message);
  687. } finally {
  688. document.body.style.cursor = 'default';
  689. }
  690. }
  691.  
  692. /**
  693. * Finds the story source url from the src attribute on the node and opens it
  694. * @param {HTMLElement} node DOM element node
  695. */
  696. function openStoryContent(node = document) {
  697. try {
  698. const storiesContainer = qs(node, IG_S_STORY_MEDIA_CONTAINER);
  699. const activeStoryContainer = qs(storiesContainer, '[style*="scale(1)"]');
  700. const video = qs(activeStoryContainer, 'video');
  701. const image = qs(activeStoryContainer, 'img');
  702.  
  703. if (video) {
  704. const source = getStoryVideoSrc(video);
  705. if (!source) throw new Error('Video source not available');
  706. openUrl(source);
  707. return;
  708. }
  709. if (image) {
  710. const source = getStoryImageSrc(image);
  711. if (!source) throw new Error('Video source not available');
  712. openUrl(source);
  713. return;
  714. }
  715. throw new Error('Story media source not available');
  716. } catch (exception) {
  717. const message = 'Failed to open story source';
  718. Logger.error(message, exception);
  719. Logger.alert(message);
  720. }
  721. }
  722.  
  723. /**
  724. * Fetches the carousel data from the IG API and opens the url of the current index
  725. * @param {string} postRelativeUrl url of the post
  726. * @param {number} carouselIndex current index of the post carousel
  727. */
  728. async function openCarouselPostMediaSource(postRelativeUrl, carouselIndex) {
  729. if (cachedApiData.post.has(postRelativeUrl)) {
  730. const cachedData = cachedApiData.post.get(postRelativeUrl)[carouselIndex];
  731. const url = getUrlFromVideoPostApiResponse(cachedData);
  732. openUrl(url);
  733. return;
  734. }
  735.  
  736. document.body.style.cursor = 'wait';
  737. const response = await httpGETRequest(API.IG_INFO_API(postRelativeUrl));
  738. const carouselMediaItems = response.items[0].carousel_media;
  739. const url = getUrlFromVideoPostApiResponse(carouselMediaItems[carouselIndex]);
  740. openUrl(url);
  741. cachedApiData.post.set(postRelativeUrl, carouselMediaItems);
  742. }
  743.  
  744. /**
  745. * Gets the source url of a post from the src attribute on the node and opens it
  746. * @param {HTMLElement} node DOM element node containing the post
  747. */
  748. async function openPostSource(node = qs(document, IG_S_SINGLE_POST_CONTAINER)) {
  749. /* if is on single post page and the node is null, the picture container can be found, since there's only one */
  750. if (node == null) return;
  751.  
  752. try {
  753. const postRelativeUrl = qs(node, IG_S_POST_TIME_ELEMENT)?.closest('a[role="link"]').getAttribute('href');
  754. if (checkPostIsCarousel(node)) {
  755. await openCarouselPostMediaSource(postRelativeUrl, getCarouselIndex(node));
  756. } else {
  757. await openSinglePostMediaSource(node, postRelativeUrl);
  758. }
  759. } catch (error) {
  760. const message = 'Failed to open post source';
  761. Logger.error(message, error);
  762. Logger.alert(message);
  763. } finally {
  764. document.body.style.cursor = 'default';
  765. }
  766. }
  767.  
  768. /** Maps the response of the IG api for reels to a more friendly format */
  769. function getUrlFromVideoPostApiResponse(apiDataItems) {
  770. const getImageOrVideoUrl = ({ video_versions, image_versions2, original_height, original_width }) => {
  771. return getUrlFromBestSource(video_versions || image_versions2.candidates, original_width, original_height);
  772. };
  773.  
  774. if (Array.isArray(apiDataItems)) return apiDataItems.map(getImageOrVideoUrl);
  775. return getImageOrVideoUrl(apiDataItems);
  776. }
  777.  
  778. /**
  779. * Gets the source url of a post from the src attribute on the node and opens it
  780. * @param {HTMLElement} node DOM element node containing the post
  781. * @param {string} postRelativeUrl url of the post
  782. */
  783. async function openSinglePostMediaSource(node, postRelativeUrl) {
  784. const imageElement = qs(node, IG_S_POST_IMG);
  785. const videoElement = qs(node, IG_S_POST_VIDEO);
  786.  
  787. if (imageElement) {
  788. openUrl(imageElement.getAttribute('src'));
  789. return;
  790. }
  791.  
  792. if (videoElement) {
  793. /* video url is available on the element */
  794. const videoSrc = videoElement.getAttribute('src');
  795. if (!videoSrc?.startsWith('blob')) {
  796. openUrl(videoSrc);
  797. return;
  798. }
  799.  
  800. if (!postRelativeUrl) {
  801. throw new Error('No post relative url found');
  802. }
  803.  
  804. /* try to get the video url using the IG api */
  805. if (cachedApiData.post.has(postRelativeUrl)) {
  806. openUrl(cachedApiData.post.get(postRelativeUrl));
  807. return;
  808. }
  809.  
  810. document.body.style.cursor = 'wait';
  811. const response = await httpGETRequest(API.IG_INFO_API(postRelativeUrl));
  812. const url = getUrlFromVideoPostApiResponse(response.items);
  813. openUrl(url);
  814. cachedApiData.post.set(postRelativeUrl, url);
  815. return;
  816. }
  817.  
  818. throw new Error('Failed to open source, no media found');
  819. }
  820.  
  821. /**
  822. * Fetches the user's profile picture from the IG API and returns it.
  823. * @param {string} username
  824. * @param {boolean} cacheFirst Whether to check the cache before making the request
  825. */
  826. async function getProfilePicture(username, cacheFirst = true) {
  827. if (cacheFirst && cachedApiData.userProfilePicture.has(username)) {
  828. Logger.log('[CACHE HIT] Profile picture');
  829. return cachedApiData.userProfilePicture.get(username);
  830. }
  831.  
  832. Logger.log("Getting user's profile picture from user info API");
  833. const url = await getProfilePictureFromUserInfoApi(username);
  834. if (!url) {
  835. Logger.error("Couldn't get profile picture url from user info API");
  836. return null;
  837. }
  838. cachedApiData.userProfilePicture.set(username, url);
  839. return url;
  840. }
  841.  
  842. /**
  843. * Tries to get the source URL of the user's profile picture using multiple methods
  844. * Opens the image or shows an alert if it doesn't find any URL
  845. */
  846. async function openProfilePicture() {
  847. try {
  848. const username = getProfileUsername();
  849. if (!username) throw new Error("Couldn't find username");
  850.  
  851. document.body.style.cursor = 'wait';
  852. const pictureUrl = await getProfilePicture(username);
  853. if (!pictureUrl) throw new Error('No profile picture found on any of the external sources');
  854.  
  855. Logger.log('Profile picture found, opening it...');
  856. openUrl(pictureUrl);
  857. } catch (error) {
  858. const message = "Couldn't get user's profile picture";
  859. Logger.error(message, error);
  860. Logger.alert(message);
  861. } finally {
  862. document.body.style.cursor = 'default';
  863. }
  864. }
  865.  
  866. /**
  867. * Get the source url of a story video
  868. * @param {HTMLElement} video DOM element node containing the video
  869. */
  870. function getStoryVideoSrc(video) {
  871. try {
  872. const videoElement = qs(video, 'source');
  873. return videoElement ? videoElement.getAttribute('src') : null;
  874. } catch (error) {
  875. Logger.error('Failed to get story video source', error);
  876. return null;
  877. }
  878. }
  879.  
  880. /**
  881. * Get the source url of a story image
  882. * @param {HTMLElement} image DOM element node containing the image
  883. */
  884. function getStoryImageSrc(image) {
  885. const fallbackUrl = image.getAttribute('src');
  886. try {
  887. const srcs = image.getAttribute('srcset').split(',');
  888. const sources = srcs.map((src) => {
  889. const [url, size] = src.split(' ');
  890. return { url, size: parseInt(size.replace(/[^0-9.,]/g, '')) };
  891. });
  892. /* get the url of the image with the biggest size */
  893. const biggestSource = sources?.reduce((biggestSrc, src) => {
  894. return biggestSrc.size > src.size ? biggestSrc : src;
  895. }, sources[0]);
  896. return biggestSource?.url ?? fallbackUrl;
  897. } catch (error) {
  898. Logger.error('Failed to get story image source', error);
  899. return fallbackUrl || null;
  900. }
  901. }
  902.  
  903. /**
  904. * Finds the best image/video source (size and quality) and returns its url.
  905. * @param {{ width: number; height: number; url: string; type: number; }[]} imageSources
  906. * @param {number | undefined} originalWidth
  907. * @param {number | undefined} originalHeight
  908. * @returns string
  909. */
  910. function getUrlFromBestSource(imageSources, originalWidth, originalHeight) {
  911. let largestSource = imageSources[0];
  912.  
  913. for (const source of imageSources) {
  914. const { width, height, type } = source;
  915. if (width === originalWidth && height === originalHeight) {
  916. largestSource = source;
  917. break;
  918. }
  919. if (height > largestSource.height || (height === largestSource.height && type > largestSource.type)) {
  920. largestSource = source;
  921. }
  922. }
  923.  
  924. return largestSource?.url;
  925. }
  926.  
  927. /**
  928. * Maps the response of the IG api for stories to a more friendly format
  929. * @param {any[]} apiDataItems
  930. */
  931. function mapStoriesApiResponse(apiDataItems) {
  932. return apiDataItems.map(({ taken_at, video_versions, image_versions2, original_width, original_height }) => {
  933. const timestamp = taken_at * 1000;
  934. const imageUrl = getUrlFromBestSource(image_versions2.candidates, original_width, original_height);
  935.  
  936. return {
  937. url: video_versions ? getUrlFromBestSource(video_versions, original_width, original_height) : imageUrl,
  938. thumbnailUrl: imageUrl,
  939. dateTime: new Date().toISOString(),
  940. relativeTime: getRelativeTime(timestamp),
  941. timestamp,
  942. };
  943. });
  944. }
  945.  
  946. /**
  947. * Fetches the current stories from a user
  948. * @param {string} username
  949. * @param {boolean} cacheFirst Whether to check the cache before making the request
  950. * @returns {Promise<{ url: string; thumbnailUrl: string; dateTime: string; relativeTime: string }[]>}
  951. */
  952. async function getUserStories(username, cacheFirst = true) {
  953. if (cacheFirst && cachedApiData.userStories.has(username)) {
  954. Logger.log('[CACHE HIT] User stories');
  955. return cachedApiData.userStories.get(username);
  956. }
  957.  
  958. Logger.log('Getting user stories...');
  959. const { id: userId } = await getUserDataFromIG(username);
  960. const result = await httpGETRequest(API.IG_REELS_FEED_API(userId), {
  961. headers: {
  962. 'User-Agent': USER_AGENT,
  963. 'x-ig-app-id': IG_APP_ID,
  964. },
  965. });
  966.  
  967. const mappedStories = mapStoriesApiResponse(result.reels[userId].items);
  968. cachedApiData.userStories.set(username, mappedStories);
  969.  
  970. return mappedStories;
  971. }
  972.  
  973. /**
  974. * Returns the user's profile picture, obtained from the user info API or __a
  975. * This uses the sessionid to get a high res version of the picture, if it was provided on the developer
  976. * options. If no sessionid was provided, it falls back to the low res version, if available.
  977. * @returns {Promise<string|null>} URL of the profile picture or null if it fails
  978. */
  979. async function getProfilePictureFromUserInfoApi() {
  980. const username = getProfileUsername();
  981. if (!username) return null;
  982.  
  983. const user = await getUserDataFromIG(username);
  984. const lowResPictureUrl = user?.profile_pic_url_hd || user?.profile_pic_url;
  985.  
  986. const userApiInfo = user?.id
  987. ? await httpGETRequest(API.IG_USER_INFO_API(user.id), {
  988. headers: {
  989. 'User-Agent': USER_AGENT,
  990. ...(sessionId ? { Cookie: `sessionid=${sessionId}` } : {}),
  991. },
  992. })
  993. : undefined;
  994.  
  995. const highResPictureUrl =
  996. 'user' in userApiInfo
  997. ? userApiInfo.user.hd_profile_pic_url_info?.url || userApiInfo.user.profile_pic_url
  998. : userApiInfo.graphql.user.hd_profile_pic_url_info.url;
  999.  
  1000. if (!highResPictureUrl) {
  1001. if (!lowResPictureUrl) {
  1002. Logger.error("Unable to get user's profile picture");
  1003. return null;
  1004. }
  1005. Logger.error("Unable to get user's high-res profile picture, falling back to to low-res...");
  1006. if (sessionId) {
  1007. Logger.warn(
  1008. "Make sure you are logged in and using a session id that hasn't expired or been revoked (logged out)"
  1009. );
  1010. }
  1011. return lowResPictureUrl;
  1012. }
  1013. return highResPictureUrl;
  1014. }
  1015.  
  1016. /**
  1017. * Return the data from a certain user using IG's __a=1 API
  1018. * @type {(username: string, cacheFirst: boolean) => Promise<{ id: string; profile_pic_url_hd: string; profile_pic_url: string; } | null>}
  1019. */
  1020. async function getUserDataFromIG(username, cacheFirst = true) {
  1021. if (cacheFirst && cachedApiData.userInfo.has(username)) {
  1022. Logger.log('[CACHE HIT] User data from IG __A1');
  1023. return cachedApiData.userInfo.get(username);
  1024. }
  1025.  
  1026. const userInfo = (
  1027. await httpGETRequest(API.IG_INFO_API(`/${username}`), {
  1028. headers: { 'User-Agent': USER_AGENT },
  1029. })
  1030. )?.graphql.user;
  1031. cachedApiData.userInfo.set(username, userInfo);
  1032. return userInfo;
  1033. }
  1034.  
  1035. /**
  1036. * Loads the key bind to open a single post or a story from storage into a global scope variable, in order
  1037. * to be used on the key binding handler method
  1038. */
  1039. async function loadPostStoryKeyBindings() {
  1040. const kbName = 'single post and story';
  1041. try {
  1042. const kb = await loadKeyBindingFromStorage(STORAGE_KEY_POST_STORY_KB, DEFAULT_KB_POST_STORY, kbName);
  1043. if (kb) openPostStoryKeyBinding = kb;
  1044. } catch (error) {
  1045. Logger.error(
  1046. `Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_KB_POST_STORY})`,
  1047. error
  1048. );
  1049. }
  1050. }
  1051.  
  1052. /**
  1053. * Loads the key bind to open a profile picture from storage into a global scope variable in order
  1054. * to be used on the key binding handler method
  1055. */
  1056. async function loadProfilePictureKeyBindings() {
  1057. const kbName = 'profile picture';
  1058. try {
  1059. const kb = await loadKeyBindingFromStorage(STORAGE_KEY_PROFILE_PICTURE_KB, DEFAULT_KB_PROFILE_PICTURE, kbName);
  1060. if (kb) openProfilePictureKeyBinding = kb;
  1061. } catch (error) {
  1062. Logger.error(
  1063. `Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_KB_PROFILE_PICTURE})`,
  1064. error
  1065. );
  1066. }
  1067. }
  1068.  
  1069. /**
  1070. * Loads a key binding from storage, if it fails or doesn't have anything stores, returns the fallback key binding
  1071. * @param {string} storageKey Unique name used to store the key binding
  1072. * @param {string} defaultKeyBinding Fallback key binding
  1073. * @param {string} keyBindingName Key binding name to show on log messages, just for context
  1074. * @returns {Promise<string|null>} The saved letter used on the key binding or null if it fails
  1075. */
  1076. async function loadKeyBindingFromStorage(storageKey, defaultKeyBinding, keyBindingName) {
  1077. let kb = await callGMFunction('getValue', storageKey, defaultKeyBinding);
  1078. if (kb === null) {
  1079. kb = defaultKeyBinding;
  1080. Logger.log(`Falling back to default key binding: Alt + ${defaultKeyBinding}`);
  1081. }
  1082.  
  1083. try {
  1084. if (isKeyBindingValid(kb)) {
  1085. const newKey = kb.toUpperCase();
  1086. Logger.log(`Discovered ${keyBindingName} key binding: Alt + ${newKey}`);
  1087. return newKey;
  1088. } else {
  1089. Logger.error(
  1090. `Couldn't load "${keyBindingName}" key binding, "${kb}" key is invalid, using default (Alt + ${defaultKeyBinding})`
  1091. );
  1092. return defaultKeyBinding;
  1093. }
  1094. } catch (error) {
  1095. if (kb != defaultKeyBinding) {
  1096. Logger.error(
  1097. `Failed to load "${keyBindingName}" key binding, falling back to default: Alt + ${defaultKeyBinding}`,
  1098. error
  1099. );
  1100. }
  1101. return null;
  1102. }
  1103. }
  1104.  
  1105. /**
  1106. * Adds event listener(s) to the current document meant to handle key presses on a single post page
  1107. */
  1108. function setupSinglePostEventListeners() {
  1109. setupKBEventListener(
  1110. isSinglePostKeyBindingSetup,
  1111. loadPostStoryKeyBindings,
  1112. handleSinglePostKeyPress,
  1113. () => {
  1114. isSinglePostKeyBindingSetup = true;
  1115. },
  1116. 'Defined single post opening event listener'
  1117. );
  1118. }
  1119.  
  1120. /**
  1121. * Adds event listener(s) to the current document meant to handle key presses on a story page
  1122. */
  1123. function setupStoryEventListeners() {
  1124. setupKBEventListener(
  1125. isStoryKeyBindingSetup,
  1126. loadPostStoryKeyBindings,
  1127. handleStoryKeyPress,
  1128. () => {
  1129. isStoryKeyBindingSetup = true;
  1130. },
  1131. 'Defined story opening event listener'
  1132. );
  1133. }
  1134.  
  1135. /** Adds event listener(s) to the current document meant to handle key presses on a profile page */
  1136. function setupProfileEventListeners() {
  1137. setupKBEventListener(
  1138. isProfileKeyBindingSetup,
  1139. loadProfilePictureKeyBindings,
  1140. handleProfileKeyPress,
  1141. () => {
  1142. isProfileKeyBindingSetup = true;
  1143. },
  1144. 'Defined profile picture opening event listener'
  1145. );
  1146. }
  1147.  
  1148. /**
  1149. * Generic method to add an event listener for a key binding
  1150. * @param {boolean} condition Condition that determines if the event should be added
  1151. * @param {() => Promise<void>} loadingFn Async function used to load the key binding
  1152. * @param {() => void} keyPressHandler Handler function for the event (key binding press)
  1153. * @param {() => void} callback Function to call after adding the event listener
  1154. * @param {string} logMessage Message logged after adding the event listener
  1155. */
  1156. async function setupKBEventListener(condition, loadingFn, keyPressHandler, callback, logMessage) {
  1157. if (condition) return;
  1158.  
  1159. await loadingFn();
  1160. document.addEventListener('keydown', keyPressHandler);
  1161. callback();
  1162. Logger.log(logMessage);
  1163. }
  1164.  
  1165. /** Removes the previously added event listener(s) meant to handle key presses on a single post page */
  1166. function removeSinglePostEventListeners() {
  1167. removeKBEventListeners(
  1168. isSinglePostKeyBindingSetup,
  1169. handleSinglePostKeyPress,
  1170. () => {
  1171. isSinglePostKeyBindingSetup = false;
  1172. },
  1173. 'Removed single post opening event listener'
  1174. );
  1175. }
  1176.  
  1177. /** Removes the previously added event listener(s) meant to handle key presses on a story page */
  1178. function removeStoryEventListeners() {
  1179. removeKBEventListeners(
  1180. isStoryKeyBindingSetup,
  1181. handleStoryKeyPress,
  1182. () => {
  1183. isStoryKeyBindingSetup = false;
  1184. },
  1185. 'Removed story opening event listener'
  1186. );
  1187. }
  1188.  
  1189. /** Removes the previously added event listener(s) meant to handle key presses on a profile page */
  1190. function removeProfileEventListeners() {
  1191. removeKBEventListeners(
  1192. isProfileKeyBindingSetup,
  1193. handleProfileKeyPress,
  1194. () => {
  1195. isProfileKeyBindingSetup = false;
  1196. },
  1197. 'Removed profile picture opening event listener'
  1198. );
  1199. }
  1200.  
  1201. /**
  1202. * Generic method to remove an event listener for a key binding
  1203. * @param {boolean} condition Condition that determines if the event should be removed
  1204. * @param {() => void} keyPressHandler Handler function previously assigned to the event
  1205. * @param {() => void} callback Function to call after removing the event listener
  1206. * @param {string} logMessage Message logged after removing the event listener
  1207. */
  1208. function removeKBEventListeners(condition, keyPressHandler, callback, logMessage) {
  1209. if (!condition) return;
  1210. document.removeEventListener('keydown', keyPressHandler);
  1211. callback();
  1212. Logger.log(logMessage);
  1213. }
  1214.  
  1215. /**
  1216. * Handles key up events on a story page
  1217. * @param {KeyboardEvent} event Keyboard event
  1218. */
  1219. function handleStoryKeyPress(event) {
  1220. handleKeyPress(
  1221. event,
  1222. openPostStoryKeyBinding,
  1223. () => pages.story.isVisible(),
  1224. 'Detected source opening shortcut on a story page',
  1225. openStoryContent
  1226. );
  1227. }
  1228.  
  1229. /**
  1230. * Handles key up events on a single post page
  1231. * @param {KeyboardEvent} event Keyboard even
  1232. */
  1233. function handleSinglePostKeyPress(event) {
  1234. handleKeyPress(
  1235. event,
  1236. openPostStoryKeyBinding,
  1237. () => pages.post.isVisible(),
  1238. 'Detected source opening shortcut on a single post page',
  1239. openPostSource
  1240. );
  1241. }
  1242.  
  1243. /**
  1244. * Handles key up events on a profile page
  1245. * @param {KeyboardEvent} event Keyboard even
  1246. */
  1247. function handleProfileKeyPress(event) {
  1248. handleKeyPress(
  1249. event,
  1250. openProfilePictureKeyBinding,
  1251. () => !(pages.story.isVisible() || pages.post.isVisible()),
  1252. 'Detected profile picture opening shortcut on a profile page',
  1253. openProfilePicture
  1254. );
  1255. }
  1256.  
  1257. /**
  1258. * Handles key up with the alt key events on certain conditions and performs an action
  1259. * @param {KeyboardEvent} event Keyboard event
  1260. * @param {string} keyBinding Target key binding (letter)
  1261. * @param {() => boolean} checkConditionsAreMet Function that determines if the conditions are met
  1262. * @param {string} logMessageString Message logged when the keybinding is used and the conditions are met
  1263. * @param {() => void} keyPressAction Function executed when the keybinding is used and the conditions are met
  1264. */
  1265. function handleKeyPress(event, keyBinding, checkConditionsAreMet, logMessageString, keyPressAction) {
  1266. if (event.altKey && event.code.toLowerCase() === `key${keyBinding.toLowerCase()}` && checkConditionsAreMet()) {
  1267. Logger.log(logMessageString);
  1268. keyPressAction();
  1269. }
  1270. }
  1271.  
  1272. /**
  1273. * Performs an HTTP GET request using the GM_xmlhttpRequest or GM.xmlHttpRequest function
  1274. * @type {<T = any>(url: string, options?: { headers?: GM.Request['headers']; parseToJson?: boolean }) => Promise<T>}
  1275. */
  1276. function httpGETRequest(url, options) {
  1277. const { headers, parseToJson = true } = options || {};
  1278.  
  1279. return new Promise((resolve, reject) => {
  1280. /** @type {GM.Request} */
  1281. const requestOptions = {
  1282. method: 'GET',
  1283. url,
  1284. headers,
  1285. timeout: 15000,
  1286. onload: (res) => {
  1287. if (res.status && res?.status !== 200) {
  1288. reject('Status Code', res?.status, res?.statusText || '');
  1289. return;
  1290. }
  1291. let data = res.responseText;
  1292. if (parseToJson) {
  1293. data = JSON.parse(res.responseText);
  1294. }
  1295. resolve(data);
  1296. },
  1297. onerror: (error) => {
  1298. error(`Failed to perform GET request to ${url}`, error);
  1299. reject(error);
  1300. },
  1301. ontimeout: () => {
  1302. Logger.error('GET Request Timeout');
  1303. reject('GET Request Timeout');
  1304. },
  1305. onabort: () => {
  1306. Logger.error('GET Request Aborted');
  1307. reject('GET Request Aborted');
  1308. },
  1309. };
  1310.  
  1311. const fnResponse = callGMFunction('xmlHttpRequest', requestOptions);
  1312. if (fnResponse === null) {
  1313. Logger.error(`Failed to perform GET request to ${url}`);
  1314. reject();
  1315. }
  1316. });
  1317. }
  1318.  
  1319. /**
  1320. * Opens a URL depending on the behavior defined in the settings
  1321. * @param {string} url URL to open
  1322. */
  1323. function openUrl(url) {
  1324. if (openSourceBehavior === BUTTON_BEHAVIOR_NEW_TAB_BG) {
  1325. callGMFunction('openInTab', url, true);
  1326. } else if (openSourceBehavior === BUTTON_BEHAVIOR_REDIR) {
  1327. window.location.replace(url);
  1328. } else {
  1329. window.open(url, '_blank');
  1330. }
  1331. }
  1332.  
  1333. /**
  1334. * Calls a both formats of a given GreaseMonkey method, for compatibility.
  1335. * @type {<T extends keyof typeof GM>(gmFunctionName: T, ...args: Parameters<typeof GM[T]>) => ReturnType<typeof GM[T]>}
  1336. */
  1337. async function callGMFunction(gmFunctionName, ...args) {
  1338. for (const fnName of [`GM.${gmFunctionName}`, `GM_${gmFunctionName}`]) {
  1339. try {
  1340. const fn = eval(fnName);
  1341. if (typeof fn !== 'function') throw new Error('Not found');
  1342. return await fn(...args);
  1343. } catch (error) {
  1344. Logger.warn(`Failed to call ${fnName} function.`, error);
  1345. }
  1346. }
  1347. Logger.error(`Failed to call all GM function variants of '${gmFunctionName}'`);
  1348. return null;
  1349. }
  1350.  
  1351. /**
  1352. * Finds the current position on a post carousel
  1353. * @param {HTMLElement} node DOM element node containing the post
  1354. * @return {number} current index
  1355. */
  1356. function getCarouselIndex(node) {
  1357. const indicators = qsa(
  1358. node,
  1359. pages.post.isVisible() ? IG_S_MULTI_HORIZONTAL_POST_INDICATOR : IG_S_MULTI_VERTICAL_POST_INDICATOR
  1360. );
  1361. for (let i = 0; i < indicators.length; i++) {
  1362. if (indicators[i].classList.length > 1) return i;
  1363. }
  1364. return -1;
  1365. }
  1366.  
  1367. /**
  1368. * Check if the key is valid to be used as a key binding
  1369. * @param {string} key Key binding key
  1370. * @returns {boolean} If it's valid or not
  1371. */
  1372. function isKeyBindingValid(key) {
  1373. return /[a-zA-Z]/gm.test(key);
  1374. }
  1375.  
  1376. /**
  1377. * Matches a CSS selector against a DOM element object to check if the element exist in the node
  1378. * @param {string} selector
  1379. * @param {HTMLElement} node DOM element node to match
  1380. * @returns {boolean} True if the element exists in the node, otherwise false
  1381. */
  1382. function elementExistsInNode(selector, node) {
  1383. return node && qs(node, selector) != null;
  1384. }
  1385.  
  1386. /**
  1387. * Returns the last 4 digits of a provided string
  1388. * @param {string} str
  1389. */
  1390. function getLast4Digits(str) {
  1391. return str?.slice(Math.max(str.length - 4, 0), str.length);
  1392. }
  1393.  
  1394. /**
  1395. * Checks if the user is logged in
  1396. * @return {boolean} whether the user is logged in or not
  1397. */
  1398. function checkIsLoggedIn() {
  1399. return Boolean(getCookie(COOKIE_IG_USER_ID));
  1400. }
  1401.  
  1402. /**
  1403. * Checks wether an Instagram post is has multiple images (carousel)
  1404. * @param {HTMLElement} postContainerNode DOM element node containing the post
  1405. */
  1406. function checkPostIsCarousel(postContainerNode) {
  1407. return qsa(postContainerNode, '[aria-label="Go back"],[aria-label="Next"]').length > 0;
  1408. }
  1409.  
  1410. /**
  1411. * Returns an existing cookie by matching it's name
  1412. * @param {string} name name of the cookie
  1413. * @returns {string} value of the cookie
  1414. */
  1415. function getCookie(name) {
  1416. const matches = document.cookie.match(PATTERN.COOKIE_VALUE(name));
  1417. return matches?.[2];
  1418. }
  1419.  
  1420. /**
  1421. * Returns the username of the user from the profile page title or the url as fallback
  1422. * @return {string} username
  1423. */
  1424. function getProfileUsername() {
  1425. const pageUsername = qs(document, IG_S_PROFILE_USERNAME_TITLE)?.innerText;
  1426. const isNotUsername = !PATTERN.IG_VALID_USERNAME.test(pageUsername);
  1427. if (isNotUsername) {
  1428. const urlPathParts = window.location.pathname.match(PATTERN.URL_PATH_PARTS);
  1429. return urlPathParts.length >= 2 ? urlPathParts[1] : null;
  1430. }
  1431. return pageUsername;
  1432. }
  1433.  
  1434. /**
  1435. * Creates an element from a given HTML string
  1436. * @param {string} htmlString HTML string to create the element from
  1437. * @returns {Element | null} The created `Element` or `null` on fail
  1438. */
  1439. function createElementFromHtml(htmlString) {
  1440. const div = document.createElement('div');
  1441. div.innerHTML = htmlString.trim();
  1442. return div.firstElementChild;
  1443. }
  1444.  
  1445. /**
  1446. * Query Selector
  1447. * @param {HTMLElement} node
  1448. * @param {string} selector
  1449. * @returns {Element | null} An `Element` or `null` if not found
  1450. */
  1451. function qs(node, selector) {
  1452. return node.querySelector(selector);
  1453. }
  1454.  
  1455. /**
  1456. * Query Selector All
  1457. * @param {HTMLElement} node
  1458. * @param {string} selector
  1459. * @returns {NodeListOf<Element>} A list with the elements that were found
  1460. */
  1461. function qsa(node, selector) {
  1462. return node.querySelectorAll(selector);
  1463. }
  1464.  
  1465. /**
  1466. * Query Selector & Add Event Listener
  1467. * @param {HTMLElement} node
  1468. * @param {string} selector
  1469. * @param {string} type
  1470. * @param {EventListener} listener
  1471. */
  1472. function qsael(node, selector, type, listener) {
  1473. const element = qs(node, selector);
  1474. element.addEventListener(type, listener);
  1475. return element;
  1476. }
  1477.  
  1478. /**
  1479. * Executes the prevent default before the passed function, passing the event down to it
  1480. * @type {<Fn>(callback: Fn) => void}
  1481. */
  1482. function withPreventDefault(callback) {
  1483. /** @param {Event | undefined} event */
  1484. return (event) => {
  1485. event?.preventDefault();
  1486. callback(event);
  1487. };
  1488. }
  1489.  
  1490. /**
  1491. * Executes the stop propagation before the passed function, passing the event down to it
  1492. * @type {<Fn>(callback: Fn) => void}
  1493. */
  1494. function withStopPropagation(callback) {
  1495. /** @param {Event | undefined} event */
  1496. return (event) => {
  1497. event?.stopPropagation();
  1498. callback(event);
  1499. };
  1500. }
  1501.  
  1502. const TIME_UNITS = [
  1503. { unit: 'year', ms: 31536000000 },
  1504. { unit: 'month', ms: 2628000000 },
  1505. { unit: 'day', ms: 86400000 },
  1506. { unit: 'hour', ms: 3600000 },
  1507. { unit: 'minute', ms: 60000 },
  1508. { unit: 'second', ms: 1000 },
  1509. ];
  1510. const RTF = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
  1511.  
  1512. /**
  1513. * Converts a timestamp into a relative human readable time (e.g. 3 hours ago)
  1514. * @param {number} timestamp
  1515. * @returns string
  1516. */
  1517. function getRelativeTime(timestamp) {
  1518. const elapsed = timestamp - Date.now();
  1519. for (const { unit, ms } of TIME_UNITS) {
  1520. if (Math.abs(elapsed) > ms || unit === 'second') {
  1521. return RTF.format(Math.floor(elapsed / ms), unit);
  1522. }
  1523. }
  1524. }
  1525.  
  1526. /** Utility that creates an iternal object and methods to use as a cache */
  1527. function buildCache() {
  1528. const keyValueRecord = {};
  1529.  
  1530. /** @type {(key: string) => string | undefined} */
  1531. const get = (key) => keyValueRecord[key];
  1532. /** @type {(key: string, value: any) => void} */
  1533. const set = (key, value) => (keyValueRecord[key] = value);
  1534. /** @type {(key: string) => boolean} */
  1535. const has = (key) => !!get(key);
  1536.  
  1537. return { get, set, has };
  1538. }
  1539.  
  1540. /**
  1541. * Logging utils generator
  1542. * @param {string} loggingTag
  1543. */
  1544. function createLogger(loggingTag) {
  1545. const logs = [];
  1546.  
  1547. const baseAlert = (...args) => alert(`${SCRIPT_NAME}:\n\n${args.join(' ')}`);
  1548. const baseLog = (type, shouldLog, ...args) => {
  1549. logs.push(`[${type.toUpperCase()}] ${args}`);
  1550. if (!shouldLog) return;
  1551. console[type]?.(`[${loggingTag}]`, ...args);
  1552. };
  1553.  
  1554. return {
  1555. logs,
  1556. log: (...args) => baseLog('log', LOGGING_ENABLED, ...args),
  1557. warn: (...args) => baseLog('warn', LOGGING_ENABLED, ...args),
  1558. error: (...args) => baseLog('error', LOGGING_ENABLED, ...args),
  1559. alert: (...args) => baseAlert(...args),
  1560. alertAndLog: (...args) => {
  1561. baseLog('log', LOGGING_ENABLED, ...args);
  1562. baseAlert(...args);
  1563. },
  1564. force: {
  1565. log: (...args) => baseLog('log', true, ...args),
  1566. warn: (...args) => baseLog('warn', true, ...args),
  1567. error: (...args) => baseLog('error', true, ...args),
  1568. alert: (...args) => baseAlert(...args),
  1569. alertAndLog: (...args) => {
  1570. baseLog('log', true, ...args);
  1571. baseAlert(...args);
  1572. },
  1573. }
  1574. };
  1575. }
  1576.  
  1577. /** Appends the necessary styles to DOM */
  1578. function injectStyles() {
  1579. try {
  1580. const styles = `
  1581. :root{--iso-post-btn-icon:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAEkklEQVR4nO3b24tVdRQH8M+ZGcemwkgJqocoy6QLXTBLp5hKMEQposIeulD0L0gRURB0Af+GLj4ERj1E0EtoZJlMF7OUJIqaSi2zcSwsRUfH08PPgcM+v33mnH07R5wv/BjY7P1dl/3ba62zfmuYxdmNWoWy5mHBaZlHcfj0366iDAecj+UN62bB8IHIvb/jR3yFLdiK/0rQqXTUMIINOIJ6xnUUG7EafVUakBU1rMUPshudtnbjcfRXZk2HWIJPFG94cn2D4Ypsagv9eAVTOjPkH4zhZ+zHiQ6ePYX1mFOBfS1xETaZWeFxvIGncK14AJyDxXgUr+FAG7yjuKQUy9rAIuyZQcEP8RAGM/APYA02zyBjDFfnsCMTLsNvLZTaiRUFyhvG9hbyDqjQCRcLuTqmyAk8rZxI3Yd1mEyRPaaCz6Ef21IU+BN3l60AbpMeH0aVHBifTxG8DwvLFJzAVfglRZf1ZQldIr79/hIie9W4UkifsRS5vGhhNeyICDuBZUUL6wDLxF/K1wqOQ/dHhNTxQpFCMmKduG6PFSWgJpSfSQFfiBc0VaNfPEXuVtAPqFUR8rpi83xeDIvruLoI8g0R4k+LIC4YsYpxY17SQRyKEN+bl7gErNGs5xGcl4d0dYT0b9lq+7IxIF4greqEJBk07ojc876QenoNJ/FB5PpdnZAkHXB95J7NnRBWjC2Ra0vzEI5p3lI35SEsGddo1ndPVrIhoaxMVn7n5FazPAwKn0KyNJ7bLkHjJ3Cu5jb5IRzLp2OpmMRE4loNF7RL0OiAmNeOZ1CqahyOXJvX7sONDoilul6M/knEyvOT7T7c6ICYsW17souYH7l2KAtRn/C9J6NqrsqqZAxqDtyTOjjya9wBp7A3cs/l2fUrHYs1GzsuOKItJAuhXyP33NqZTpUi1gn6thOCpAO+jNxzTyeEFSN2ZDaah3BEcwwY15sHlHNxUMF9izlCXk2SPpiHtCQ8rFnPfxUQtN+NEH+Wl7QEfKRZz9eLIL4zQlzH7UWQF4SVStYx1nTcpQeOp4V4tFO8KVoYHokIqOPZIoVkxDPiuj1QpJA+fB4Rclz4RLqFFZp//taFSZXCcYP4KcyEUIFVjRvFM9QUbilL6KsRgXXhoLJKJyyVPlLzcpmCB/FxiuAJ1XwOw+Jvfjo9l16kXYjvUxQ4JgTGMrLDAF7UepjqD2F0p3QsFAYi0hTZJd5Sz4qVWo/INK59KnRC2k6YXtuEIaks23KucAK1dQYZsbVf6BCXjvlCL34mhQ7ibTyJ68Tr8yFhCOMJvCXMEc7Eu13Y9l11wqAwKJk2vJS2xoUhyb1CyyqWz9PWJJ4TdtYiYdt31QmEOmG0AyOyru2a83zPOKFPKJvbDVidrB24T3p/r2ecMI0RvCM9Z7ezJvCmcMLbTmOz55xAqAlGhAptkzBkGes2T+EnvIeXhNZblnqiJ52QRA2XCql0gVBcDRXIf0Y4oWzMOsGsE9DaCZWVzd1GKyd85wz5h6y8aOWEK7qoV6WIOeGwMAxy1mCRsO2njV/bXXW6gz7hlPusevOzaAf/A63z45sbiSeUAAAAAElFTkSuQmCC')!important;--iso-post-carousel-btn-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2264%22%20height%3D%2264%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cg%20fill%3D%22none%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M12%202C6.477%202%202%206.477%202%2012s4.477%2010%2010%2010s10-4.477%2010-10S17.523%202%2012%202zm-1%205a4%204%200%201%200%202.032%207.446l1.76%201.761a1%201%200%200%200%201.415-1.414l-1.761-1.761A4%204%200%200%200%2011%207zm0%206a2%202%200%201%200%200-4a2%202%200%200%200%200%204z%22%20fill%3D%22white%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")!important;--iso-story-btn-icon:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAABcklEQVRYhe2WO04DMRRFrwkroIAG2oQKBAuggzolHUVCEAWrYAGUCAE9RGyAAgoWgAIUQbABCn6BNsmhiEeyRvNx4knF3Mb2vM+9fmM/WSpRIgHAEnAF/JCPZ2DeiT0D+gl+feDUl/zDg9jFphP/neHXj/PNJmg4kjRn5x1Jrzmau5JunfWOpG1JFUkbkhYcW8WnAlHZO8BMbkB6ngYwiJfAJzBCO4C8BQxtngHwliZg4h1mkDclHUsykpB0IOlunAQTVwDYi+28ab9fpB3CwgSkkVtbHfgCTqYiAGg6B24I7I8jPkgAsFsY+bgCfMmBCrCF0zGDBQDrsX/eyPA9tH7duC2pE/pioNE1Q1LLGHOe4Vuz43JhAowxD8CaJIwxT5PmCamAjDGPIfHSFDphKaAUUISAXztWCXiQRLA5oj7Qi9uTruGNpLqkVUn3wEughqqkFTu/zvUGasAnxeMdWPSSzOhl3AZ6BRD3gEtv8hL/Dn+PJax2JC/rEwAAAABJRU5ErkJggg==")!important;--iso-settings-btn-icon:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAD+UlEQVR4nO3aS4xkYxQH8F8rNDozpmcyk8hoYSEEM0IiYUkvJJN4TLcMCwsRZsXOxpL9JMaClcdGLDw34zUJKxlsxDOTiKaHYEEahWFUd1l8Lemp+m7Vvd/9bnWR+idnUzfnf87536/O97pMMMEEE+TFHhzGp/gN3UQ7hS/xOC4aaQWJmMYTWJVedJG1cfvoSqmOabwtf+EbrYO7RlVQVTyp2eL/tb+NoQh79A/7E1jElgS+WNFjLcJj+ovfXoOvV4A7jbkInzk9ucWafL0CMOYi/Or0xFKG/UbEBGCMRShKuAm+O/SL0MHdGeImY5QCMIYijFoAxkyEzRCAMRJhswRgTETYTAEonh1uy5BLKeQWoN3Dd2EJn5gIbcxlyGcocgvwfg/fa9JFOJwhn6E4goexD5dn4HtQv6ip9kWGfEaOaXwojwB/jTj3bNgtnwj/WZyNB3BMf2PMJsBUA4lvNnqLHljjGTUC3YBtCX4tXFsj7qZjDq8KSr+LmQq+U3hm3fdlzczTjfaAq7HSE+A97Crhew6e7/FdwXzmHBsV4AKsRYJ8j/uFInvRwq34POLXxS2Zc2y8CX6EvQXP2ngHXwkHpbtxo+IR0hH6yO8JeRShUhNMwVPyrdQ+zp1cJMZApMwCXyf4FOHHjFxJSBGglTF+rGeMFCkCXJox/lXyCto4WvhGvh7QxU2Zc2x0GlyMBKhrr2fOsTEBzsdSJEAOuydjno0IcB7eiJDnspPYnynX7ALsxQcR4pgtY0G4HtsqFHW8pG9H+Bpktma+WQSYFQp5Sf8526DiY8lvV61xnsSLOFjANwy1BdgnvI2qw3hhAOeBBL6utKPtSgLE1gE7pc3Nbw149mYCH6HxNoqYAHUOSYqQuiFZy5pFBLFif07kGrSvvzmBbxU/JeZSC7PC3v4F/KH8//W4eNPagW9Lcpxaj7sg7bhNhLMWZoWpqWxTPCFcWG5dtwPKzwDHcGXdhCO8WbBfmJ5SOnkZO4JzM+XaiABwb4Q8hy0JoyUXGhOAsHHJLcCg9UMKGhVgPhKgji3LP+02KkALv0SCpNqzuROMxBiIquqvCsfbubCUkSsJKcPvz4zxOxm56F86t4c5pAiwM8GnCJdk5KJ/xbk8zOHMigFmcNmA5z8IFyPfCf3iYuHMr2iau65i/EHYgUM9vx3NyI9wxRVrZp8I2+jYiJrGfYIovX5rwnVbHRStODu4oiZ3H+b1X44+JxQ5DLuE5e5G3xXF12yPKj+bxKyxD6TmhKvtLp5Wbas7I1ypd9c5ir7+eki94o/irAp5JeEaaQcn23B9wbMpPCJ+A13GOsKbb7z4JrAFr6hedFvoQYc08J+fYIIJ/t/4By9tfiJ9bFVlAAAAAElFTkSuQmCC")!important;--iso-settings-select-arrow-icon:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAnXwAAJ18BHYa6agAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAACgSURBVHic7dcxDoJAFEXRZ+zdlkuyNC7L0sINuQEtzE8sDBEEkeGchHaG+7pJAAAAAAAAWIfNiGftkuxHPK/LOcntR3f1ckhyn/g7/qxmoClH+Pv4MsUIi4kvY46wuPgyxgiLjS/fjLD4+DJkhGbiS58Rmosvn4zQbHzpGqH5+PJuhNXEl9cRZovfznVxkmuej7FLktOM/wEAAAAAAKzEA6yHqvmapYJUAAAAAElFTkSuQmCC")!important;--iso-anonymous-stories-btn:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAEU0lEQVR4nO2ay29VRRzHPy0klkei5UIFLG0XwgbiggR5llZQd0ZMVN4EwYVL9Q8o7DWwIYEVARLFIFHjE+xSFwRi6YJHwqtACJRQjECh8rjAYuaE3zmce8/MOTP3knQ+yS/33OT3+31n5sw58zoQCAQCo5gGz/lnAp8BE4BjwE/AzYyYycAKYAHwL7ADuOyxjN6YBdwGngh7ABwCNgMl4VsCPgUOAw8TMTeBtpqV2iHfEq9I0h6iKpxW6aTtqW3RizMJGKF6pWzsHvCKj4I2+kgKrAWaHOYbB3zsMJ93+nB39yM7UtMaFGAu7isf2RuuC+vjEdjkIWfERo+5ndCEGrt99YAh4CWXBXbdAz4Amh3nlJSA9zzmL0wv/u5+ZH/UrDaWtANl/DdAGYczQ5ePwCeO81WiEdhQAx0rGoGL+L/7kV2gNo1tzDvUrvKRLXNRcFet6HPsr8TmOmim4nrhY2ojOBhyXfQA1wsfU5qANXXQfQ4fCx9T66tB/QKBwCjkAPV7sfmyA2kVfaGmk/UgNEC9CxAI1BcXh6OtqN2gKagN0UvafNAGdKAWQUNa54onrao0A1uBk6QPOWeBLcQPQPNSAnqAMxW0TmgtL0dnaWxCndiajL3/UWz7ap3OYaI1hOdzgwZge4rwCHActSPchzrITPp8jf3j9lVKnntao1drpu1DbMuhZcTWhNAA6u5OTPhNANYD5xP+Wyy0ehKx51D7DuNTtDag9gilf4+FlhFvA4+FwA9avBrjge9FzCOg00CrU/vKKew4A62DIqYMvGWgZUQD8I9I3guMNYxtBH4TscfI7p5HhP9hYExOrX4cTfTkju8wMM0yfqqOi3IsqeLbKfzuAC2WWq/quCjH8qwAk9b9EnhTX+9CdWsbhlFzhAX6fxk1jH4OfAGsBLpRvep9YJ722wH8aKl1F/WR1UL9/xYOjtL6edaieZ+rLuIvKRMzeV+kIXvR8Zw5YgyJhLZdMqIF+wbIO4kqiRzXs5xNXmZymBvOWahbif+ngX3AUdRjuBD1mdwMB1q3xfXLOXPEuMyzFu3ImaNd5DhHesM3oeYWkd+MFB8TWkWOzDWJyTAxIK4X5SyUjOtHjfNJ/ie+z59Xa7G4HqjopTFpgEPiep11cZ6P+6uK398OtNaLaycfU8wiPjOzPZVdJmIfUP1F2qJ9Iv+uAlqPgNct4yuyVyS+hnqmTWjX/lHsToOYXcL/KuZfg3QAgyJ2t2GcEa2oIUU2QndGTDfxyg+iTpKzmES8IleBpRkxyxMxg8B0Ay0rlgL3hcgT4FdgNeoxada/q4FfEn53gfkWWvN1jMzxM7AK9Ql+pLUW+D3hd5/8k6hMuohPjEzsOvbPcqQle52J3SC7txSmDdhPfHmcZmXUJ/OvFdBqBb4z0HoMfEOOuUORnZPZwIfAu6gX0GTUVtlF4E/UOv5UgfySOcBHqJVpB2q6e0NoHXSoFQgEAoFAYJTwFHhgrSVd8Ek7AAAAAElFTkSuQmCC')!important;--iso-settings-separator-color:rgba(219, 219, 219, 1)!important;--iso-story-button-size:40px!important;--iso-story-button-icon-size:24px!important;--iso-post-button-size:40px!important;--iso-post-button-icon-size:24px!important;--iso-profile-button-size:40px!important;--iso-profile-button-icon-size:22px!important;--iso-profile-button-icon-hover-size:calc(var(--iso-profile-button-icon-size) + 2px)!important;--iso-setting-button-size:22px!important;--iso-setting-button-icon-size:22px!important}
  1582. .iso-flex-column{display:flex!important;flex-direction:column!important}
  1583. .iso-flex-row-center{display:flex!important;flex-direction:row!important;align-items:center!important}
  1584. .iso-settings-option-container{padding:10px 0 10px 0!important}
  1585. .${C_BTN_ANONYMOUS_STORIES},.${C_BTN_POST},.${C_BTN_PROFILE_PIC},.${C_BTN_STORY},.${C_SETTINGS_BTN},.${C_STORIES_MODAL_LIST_ITEM},.iso-settings-menu-option-button{transition:all .2s ease-in-out!important;-webkit-transition:all .2s ease-in-out!important;-moz-transition:all .2s ease-in-out!important;-ms-transition:all .2s ease-in-out!important;-o-transition:all .2s ease-in-out!important}
  1586. .${C_BTN_POST}{min-height:var(--iso-post-button-size)!important;min-width:var(--iso-post-button-size)!important;max-height:var(--iso-post-button-size)!important;max-width:var(--iso-post-button-size)!important;outline:0!important;border:none!important;cursor:pointer!important;opacity:1!important;margin-left:6px!important;margin-right:-8px!important;background-color:transparent!important;background-repeat:no-repeat!important;background-image:var(--iso-post-btn-icon)!important;background-size:var(--iso-post-button-icon-size) var(--iso-post-button-icon-size)!important;background-position:center!important}
  1587. .${C_BTN_POST}:hover{opacity:.6!important}
  1588. .${C_PROFILE_BUTTON_CONTAINER}{display:flex!important;flex-direction:row!important;justify-content:center!important;align-items:center!important;position:absolute!important;bottom:-16px!important;right:0!important;left:0!important}
  1589. .${C_BTN_ANONYMOUS_STORIES},.${C_BTN_PROFILE_PIC}{outline:0!important;min-height:var(--iso-profile-button-size)!important;min-width:var(--iso-profile-button-size)!important;max-height:var(--iso-profile-button-size)!important;max-width:var(--iso-profile-button-size)!important;border:0!important;cursor:pointer!important;padding:0!important;border:1.5px solid #000!important;border-radius:50%!important;background-color:#fff!important;background-repeat:no-repeat!important;background-size:var(--iso-profile-button-icon-size) var(--iso-profile-button-icon-size)!important;background-position:center!important}
  1590. .${C_BTN_PROFILE_PIC}{background-image:var(--iso-post-btn-icon)!important}
  1591. .${C_BTN_ANONYMOUS_STORIES}{margin-left:6px!important;background-image:var(--iso-anonymous-stories-btn)!important}
  1592. .${C_BTN_ANONYMOUS_STORIES}:hover:not(:disabled),.${C_BTN_PROFILE_PIC}:hover:not(:disabled){background-color:#e8e8e8!important;background-size:var(--iso-profile-button-icon-hover-size) var(--iso-profile-button-icon-hover-size)!important}
  1593. ${IG_S_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC},${IG_S_PRIVATE_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC}{opacity:1!important}
  1594. .${C_BTN_STORY}{position:fixed!important;top:56px!important;right:16px!important;min-width:var(--iso-story-button-size)!important;min-height:var(--iso-story-button-size)!important;max-height:var(--iso-story-button-size)!important;max-height:var(--iso-story-button-size)!important;padding:16px!important;border:none!important;cursor:pointer!important;z-index:99!important;opacity:1!important;background-color:transparent!important;background-image:var(--iso-story-btn-icon)!important;background-repeat:no-repeat!important;background-size:var(--iso-story-button-icon-size) var(--iso-story-button-icon-size)!important;background-position:center!important}
  1595. .${C_BTN_STORY}:hover{opacity:.8!important}
  1596. .${C_SETTINGS_BTN}{width:var(--iso-setting-button-size)!important;height:var(--iso-setting-button-size)!important;cursor:pointer!important;top:16px!important;border:none!important;right:16px!important;position:fixed!important;background-color:transparent!important;background-image:var(--iso-settings-btn-icon)!important;background-size:var(--iso-setting-button-icon-size) var(--iso-setting-button-icon-size)!important;opacity:.8!important;z-index:1000!important}
  1597. @media only screen and (max-width:1024px){
  1598. .${C_SETTINGS_BTN}{top:64px!important}
  1599. }
  1600. .${C_SETTINGS_BTN}:hover{opacity:1!important}
  1601. .${C_MODAL_BACKDROP}{position:fixed!important;justify-content:center!important;align-items:center!important;width:100vw!important;height:100vh!important;top:0!important;left:0!important;background-color:rgba(0,0,0,.7)!important;display:none!important;z-index:1!important}
  1602. .${C_MODAL_WRAPPER}{display:flex!important;width:320px!important;flex-direction:column!important;background-color:#fff!important;border-radius:6px!important;z-index:5!important;box-shadow:-1px 2px 14px 3px rgba(0,0,0,.5)!important}
  1603. .${C_STORIES_MODAL} .${C_MODAL_WRAPPER}{width:auto!important;max-width:calc(100vw - 124px)!important}
  1604. @media only screen and (max-width:769px){
  1605. .${C_STORIES_MODAL} .${C_MODAL_WRAPPER}{max-width:calc(100vw - 48px)!important}
  1606. }
  1607. .iso-modal-title-container{display:flex!important;flex-direction:row!important;justify-content:space-between!important;font-weight:700!important;border-bottom:1px solid var(--iso-settings-separator-color)!important}
  1608. .iso-modal-title{display:flex!important;justify-content:center!important;flex-direction:row!important;font-size:16px!important;padding:16px!important;text-align:left!important}
  1609. .${C_MODAL_CLOSE_BTN}{width:24px!important;height:24px!important;border:0!important;padding:0!important;background-color:transparent!important;margin-top:8px!important;margin-right:8px!important;cursor:pointer!important}
  1610. .iso-modal-title-link{margin-left:4px!important;color:#4287f5!important;text-decoration:none!important}
  1611. .iso-modal-content-container{display:flex!important;flex-direction:column!important;flex:1!important;border:none!important;background-color:transparent!important;font-size:14px!important;text-align:left!important;padding:8px 0!important}
  1612. .iso-settings-content-section{display:flex!important;flex-direction:column!important;flex:1!important;padding:0 16px 0 16px!important}
  1613. .iso-settings-menu-option-button{display:flex!important;flex-direction:row!important;padding:12px 16px!important;margin:0 -16px 0 -16px!important;border:none!important;background-color:transparent!important;font-size:14px!important;padding-left:16px!important;text-align:left!important;cursor:pointer!important}
  1614. .iso-settings-menu-option-button:hover{background-color:rgba(214,214,214,.3)!important}
  1615. .iso-settings-menu-option-button:active{background-color:rgba(214,214,214,.4)!important}
  1616. [for="${ID_SETTINGS_BUTTON_BEHAVIOR_SELECT}"],[for="${ID_SETTINGS_SESSION_ID_INPUT}"]{font-size:12px!important;margin-bottom:6px!important}
  1617. .${C_MODAL_WRAPPER} input[type=text],.${C_MODAL_WRAPPER} select{height:32px!important;font-size:14px!important;border:1px solid gray!important;border-radius:4px!important;padding:0 6px!important;-moz-appearance:none!important;-webkit-appearance:none!important;appearance:none!important}
  1618. #${ID_SETTINGS_BUTTON_BEHAVIOR_SELECT}{background-image:var(--iso-settings-select-arrow-icon)!important;background-size:24px 24px!important;background-repeat:no-repeat!important;background-position-x:99%!important;background-position-y:50%!important}
  1619. #${ID_SETTINGS_DEVELOPER_OPTIONS_CONTAINER}{border-top:1px solid var(--iso-settings-separator-color)!important}
  1620. #${ID_SETTINGS_DEVELOPER_OPTIONS_BTN}{display:flex!important;flex-direction:row!important;justify-content:space-between!important;align-items:center!important}
  1621. .${C_SETTINGS_SECTION_COLLAPSED}{display:none!important}
  1622. #${ID_SETTINGS_DEVELOPER_OPTIONS_BTN}.${C_SETTINGS_SECTION_COLLAPSED} .iso-settings-select-arrow{transform:rotate(-90deg)!important}
  1623. .iso-settings-select-arrow{background-color:transparent!important;background-image:var(--iso-settings-select-arrow-icon)!important;background-size:24px 24px!important;width:24px!important;height:24px!important}
  1624. .${C_STORIES_MODAL_LIST}{display:flex!important;flex-direction:row!important;overflow-x:auto!important;padding:20px 0!important}
  1625. .${C_STORIES_MODAL_LIST_ITEM}{display:flex!important;flex-direction:column!important;align-items:center!important;margin-left:16px!important;border-radius:6px!important;opacity:1!important;color:#505050!important}
  1626. .${C_STORIES_MODAL_LIST_ITEM},.${C_STORIES_MODAL_LIST_ITEM}:active,.${C_STORIES_MODAL_LIST_ITEM}:visited{text-decoration:none!important}
  1627. .${C_STORIES_MODAL_LIST_ITEM}:hover{opacity:.7!important}
  1628. .${C_STORIES_MODAL_LIST_ITEM}:last-child{margin-right:16px!important}
  1629. .${C_STORIES_MODAL_LIST_ITEM} img{height:max(256px,calc(100vh / 2))!important;object-fit:cover!important;border-radius:6px!important}
  1630. .${C_STORIES_MODAL_LIST_ITEM} time{color:#505050!important;margin-top:8px!important}
  1631. #${ID_SETTINGS_DEBUGGING_INPUT}{margin-right:8px!important}
  1632. `;
  1633. const element = document.createElement('style');
  1634. element.textContent = styles;
  1635. document.head.appendChild(element);
  1636. Logger.log('Injected CSS into DOM');
  1637. } catch (error) {
  1638. Logger.error('Failed to inject CSS into DOM', error);
  1639. }
  1640. }
  1641. })();