InoReader restore lost images and videos

Loads new images and videos from VK and Telegram in InoReader articles

  1. // ==UserScript==
  2. // @name InoReader restore lost images and videos
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.10
  5. // @description Loads new images and videos from VK and Telegram in InoReader articles
  6. // @author Kenya-West
  7. // @grant GM_registerMenuCommand
  8. // @grant GM_unregisterMenuCommand
  9. // @match https://*.inoreader.com/feed*
  10. // @match https://*.inoreader.com/article*
  11. // @match https://*.inoreader.com/folder*
  12. // @match https://*.inoreader.com/starred*
  13. // @match https://*.inoreader.com/library*
  14. // @match https://*.inoreader.com/dashboard*
  15. // @match https://*.inoreader.com/web_pages*
  16. // @match https://*.inoreader.com/trending*
  17. // @match https://*.inoreader.com/commented*
  18. // @match https://*.inoreader.com/recent*
  19. // @match https://*.inoreader.com/search*
  20. // @match https://*.inoreader.com/channel*
  21. // @match https://*.inoreader.com/teams*
  22. // @match https://*.inoreader.com/dashboard*
  23. // @match https://*.inoreader.com/pocket*
  24. // @match https://*.inoreader.com/liked*
  25. // @match https://*.inoreader.com/tags*
  26. // @icon https://inoreader.com/favicon.ico?v=8
  27. // @license MIT
  28. // ==/UserScript==
  29. // @ts-check
  30.  
  31. (function () {
  32. "use strict";
  33.  
  34. /**
  35. * @typedef {Object} appConfig
  36. * @property {Array<{
  37. * prefixUrl: string,
  38. * corsType: "direct" | "corsSh" | "corsAnywhere" | "corsFlare",
  39. * token?: string,
  40. * hidden?: boolean
  41. * }>} corsProxies
  42. */
  43. const appConfig = {
  44. corsProxies: [
  45. {
  46. prefixUrl: "https://corsproxy.io/?",
  47. corsType: "direct",
  48. },
  49. {
  50. prefixUrl: "https://proxy.cors.sh/",
  51. corsType: "corsSh",
  52. token: undefined,
  53. hidden: true,
  54. },
  55. {
  56. prefixUrl: "https://cors-anywhere.herokuapp.com/",
  57. corsType: "corsAnywhere",
  58. hidden: true,
  59. },
  60. {
  61. prefixUrl: "https://cors-1.kenyawest.workers.dev/?upstream_url=",
  62. corsType: "corsFlare",
  63. },
  64. ],
  65. };
  66.  
  67. const appState = {
  68. readerPaneMutationObserverLinked: false,
  69. restoreImagesInListView: false,
  70. restoreImagesInArticleView: false,
  71. };
  72.  
  73. // Select the node that will be observed for mutations
  74. const targetNode = document.body;
  75.  
  76. // Options for the observer (which mutations to observe)
  77. const mutationObserverGlobalConfig = {
  78. attributes: false,
  79. childList: true,
  80. subtree: true,
  81. };
  82.  
  83. const querySelectorPathArticleRoot = ".article_full_contents .article_content";
  84.  
  85. /**
  86. * Callback function to execute when mutations are observed
  87. * @param {MutationRecord[]} mutationsList - List of mutations observed
  88. * @param {MutationObserver} observer - The MutationObserver instance
  89. */
  90. const callback = function (mutationsList, observer) {
  91. for (let i = 0; i < mutationsList.length; i++) {
  92. if (mutationsList[i].type === "childList") {
  93. mutationsList[i].addedNodes.forEach(function (node) {
  94. if (node.nodeType === Node.ELEMENT_NODE) {
  95. if (appState.restoreImagesInListView) {
  96. restoreImagesInArticleList(node);
  97. }
  98. runRestoreImagesInArticleView(node);
  99. }
  100. });
  101. }
  102. }
  103. };
  104.  
  105. function registerCommands() {
  106. let enableImageRestoreInListViewCommand;
  107. let disableImageRestoreInListViewCommand;
  108. let enableImageRestoreInArticleViewCommand;
  109. let disableImageRestoreInArticleViewCommand;
  110.  
  111. const restoreImageListView = localStorage.getItem("restoreImageListView") ?? "false";
  112. const restoreImageArticleView = localStorage.getItem("restoreImageArticleView") ?? "true";
  113.  
  114. if (restoreImageListView === "false") {
  115. appState.restoreImagesInListView = false;
  116. // @ts-ignore
  117. enableImageRestoreInListViewCommand = GM_registerMenuCommand("Enable image restore in article list", () => {
  118. localStorage.setItem("restoreImageListView", "true");
  119. appState.restoreImagesInListView = true;
  120. if (enableImageRestoreInListViewCommand) {
  121. unregisterAllCommands();
  122. registerCommands();
  123. }
  124. });
  125. } else {
  126. appState.restoreImagesInListView = true;
  127. // @ts-ignore
  128. disableImageRestoreInListViewCommand = GM_registerMenuCommand("Disable image restore in article list", () => {
  129. localStorage.setItem("restoreImageListView", "false");
  130. appState.restoreImagesInListView = false;
  131. if (disableImageRestoreInListViewCommand) {
  132. unregisterAllCommands();
  133. registerCommands();
  134. }
  135. });
  136. }
  137.  
  138. if (restoreImageArticleView === "false") {
  139. appState.restoreImagesInArticleView = false;
  140. // @ts-ignore
  141. enableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Enable image restore in article view", () => {
  142. localStorage.setItem("restoreImageArticleView", "true");
  143. appState.restoreImagesInArticleView = true;
  144. if (enableImageRestoreInArticleViewCommand) {
  145. unregisterAllCommands();
  146. registerCommands();
  147. }
  148. });
  149. } else {
  150. appState.restoreImagesInArticleView = true;
  151. // @ts-ignore
  152. disableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Disable image restore in article view", () => {
  153. localStorage.setItem("restoreImageArticleView", "false");
  154. appState.restoreImagesInArticleView = false;
  155. if (disableImageRestoreInArticleViewCommand) {
  156. unregisterAllCommands();
  157. registerCommands();
  158. }
  159. });
  160. }
  161.  
  162. function unregisterCommand(command) {
  163. // @ts-ignore
  164. GM_unregisterMenuCommand(command);
  165. }
  166.  
  167. function unregisterAllCommands() {
  168. // @ts-ignore
  169. GM_unregisterMenuCommand(enableImageRestoreInListViewCommand);
  170. // @ts-ignore
  171. GM_unregisterMenuCommand(disableImageRestoreInListViewCommand);
  172. // @ts-ignore
  173. GM_unregisterMenuCommand(enableImageRestoreInArticleViewCommand);
  174. // @ts-ignore
  175. GM_unregisterMenuCommand(disableImageRestoreInArticleViewCommand);
  176. }
  177. }
  178.  
  179. //
  180. //
  181. // FIRST PART - RESTORE IMAGES IN ARTICLE LIST
  182. //
  183. //
  184. //
  185.  
  186. /**
  187. *
  188. * @param {Node} node
  189. * @returns {void}
  190. */
  191. function restoreImagesInArticleList(node) {
  192. /**
  193. * @type {MutationObserver | undefined}
  194. */
  195. let tmObserverImageRestoreReaderPane;
  196. const readerPane = document.body.querySelector("#reader_pane");
  197. if (readerPane) {
  198. if (!appState.readerPaneMutationObserverLinked) {
  199. appState.readerPaneMutationObserverLinked = true;
  200.  
  201. /**
  202. * Callback function to execute when mutations are observed
  203. * @param {MutationRecord[]} mutationsList - List of mutations observed
  204. * @param {MutationObserver} observer - The MutationObserver instance
  205. */
  206. const callback = function (mutationsList, observer) {
  207. for (let mutation of mutationsList) {
  208. if (mutation.type === "childList") {
  209. mutation.addedNodes.forEach(function (node) {
  210. if (node.nodeType === Node.ELEMENT_NODE) {
  211. setTimeout(() => {
  212. start(node);
  213. }, 500);
  214. }
  215. });
  216. }
  217. }
  218. };
  219.  
  220. // Options for the observer (which mutations to observe)
  221. const mutationObserverLocalConfig = {
  222. attributes: false,
  223. childList: true,
  224. subtree: false,
  225. };
  226.  
  227. // Create an observer instance linked to the callback function
  228. tmObserverImageRestoreReaderPane = new MutationObserver(callback);
  229.  
  230. // Start observing the target node for configured mutations
  231. tmObserverImageRestoreReaderPane.observe(readerPane, mutationObserverLocalConfig);
  232. }
  233. } else {
  234. appState.readerPaneMutationObserverLinked = false;
  235. tmObserverImageRestoreReaderPane?.disconnect();
  236. }
  237.  
  238. /**
  239. *
  240. * @param {Node} node
  241. */
  242. function start(node) {
  243. /**
  244. * @type {Node & HTMLDivElement}
  245. */
  246. // @ts-ignore
  247. const element = node;
  248. if (element.hasChildNodes() && element.id.includes("article_") && element.classList.contains("ar")) {
  249. const imageElement = getImageElement(element);
  250. if (imageElement) {
  251. const telegramPostUrl = getTelegramPostUrl(element);
  252. const imageUrl = getImageLink(imageElement);
  253. if (imageUrl) {
  254. testImageLink(imageUrl).then(async () => {
  255. const tgPost = await commonFetchTgPostEmbed(telegramPostUrl);
  256. await replaceImageSrc(imageElement, tgPost);
  257. await placeMediaCount(element, tgPost);
  258. });
  259. }
  260. }
  261. }
  262. }
  263.  
  264. /**
  265. *
  266. * @param {Node & HTMLDivElement} node
  267. * @returns {HTMLDivElement | null}
  268. */
  269. function getImageElement(node) {
  270. const nodeElement = node;
  271. /**
  272. * @type {HTMLDivElement | null}
  273. */
  274. const divImageElement = nodeElement.querySelector("a[href*='t.me'] > div[style*='background-image']");
  275. return divImageElement ?? null;
  276. }
  277.  
  278. /**
  279. *
  280. * @param {Node & HTMLDivElement} node
  281. * @returns {string}
  282. */
  283. function getTelegramPostUrl(node) {
  284. if (!node) {
  285. return "";
  286. }
  287. return getFromNode(node) ?? "";
  288.  
  289. /**
  290. *
  291. * @param {Node & HTMLDivElement} node
  292. * @returns {string}
  293. */
  294. function getFromNode(node) {
  295. /**
  296. * @type {HTMLDivElement}
  297. */
  298. // @ts-ignore
  299. const nodeElement = node;
  300. /**
  301. * @type {HTMLAnchorElement | null}
  302. */
  303. const ahrefElement = nodeElement.querySelector("a[href*='t.me']");
  304. const telegramPostUrl = ahrefElement?.href ?? "";
  305. // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
  306. try {
  307. return new URL(telegramPostUrl).origin + new URL(telegramPostUrl).pathname;
  308. } catch (error) {
  309. return telegramPostUrl?.split("?")[0];
  310. }
  311. }
  312. }
  313.  
  314. /**
  315. *
  316. * @param {HTMLDivElement} div
  317. */
  318. function getImageLink(div) {
  319. const backgroundImageUrl = div?.style.backgroundImage;
  320. return commonGetUrlFromBackgroundImage(backgroundImageUrl);
  321. }
  322.  
  323. /**
  324. *
  325. * @param {string} imageUrl
  326. * @returns {Promise<void>}
  327. */
  328. function testImageLink(imageUrl) {
  329. return new Promise((resolve, reject) => {
  330. const img = new Image();
  331. img.src = imageUrl;
  332. img.onload = function () {
  333. reject();
  334. };
  335. img.onerror = function () {
  336. resolve();
  337. };
  338. });
  339. }
  340.  
  341. /**
  342. *
  343. * @param {HTMLDivElement} div
  344. * @param {Document} tgPost
  345. * @returns {Promise<void>}
  346. */
  347. async function replaceImageSrc(div, tgPost) {
  348. const doc = tgPost;
  349. const imgLink = commonGetImgUrlsFromTgPost(doc) ?? [];
  350. if (imgLink?.length > 0) {
  351. try {
  352. div.style.backgroundImage = `url(${imgLink})`;
  353. } catch (error) {
  354. console.error(`Error parsing the HTML from the telegram post. Error: ${error}`);
  355. }
  356. } else {
  357. console.error("No image link found in the telegram post");
  358. }
  359. }
  360.  
  361. /**
  362. *
  363. * @param {HTMLDivElement} node
  364. * @param {Document} tgPost
  365. */
  366. async function placeMediaCount(node, tgPost) {
  367. const mediaCount = commonGetImgUrlsFromTgPost(tgPost);
  368. if (mediaCount.length > 1) {
  369. placeElement(mediaCount.length);
  370. }
  371.  
  372. /**
  373. * @param {string | number} total
  374. */
  375. function placeElement(total) {
  376. // Create the new element
  377. const mediaCountElement = document.createElement("span");
  378. mediaCountElement.className = "article_tile_comments";
  379. mediaCountElement.title = "";
  380. mediaCountElement.style.backgroundColor = "rgba(0,0,0,0.5)";
  381. mediaCountElement.style.padding = "0.1rem";
  382. mediaCountElement.style.borderRadius = "5px";
  383. mediaCountElement.style.marginLeft = "0.5rem";
  384. mediaCountElement.textContent = `1/${total}`;
  385.  
  386. // Find the target wrapper
  387. let wrapper = node.querySelector(".article_tile_comments_wrapper.flex");
  388.  
  389. // If the wrapper doesn't exist, create it
  390. if (!wrapper) {
  391. wrapper = document.createElement("div");
  392. wrapper.className = "article_tile_comments_wrapper flex";
  393.  
  394. // Find the parent element and append the new wrapper to it
  395. const parent = node.querySelector(".article_tile_content_wraper");
  396. if (parent) {
  397. parent.appendChild(wrapper);
  398. } else {
  399. console.error("Parent element not found");
  400. return;
  401. }
  402. }
  403.  
  404. // Append the new element to the wrapper
  405. wrapper.appendChild(mediaCountElement);
  406. }
  407. }
  408. }
  409.  
  410. //
  411. //
  412. // SECOND PART - RESTORE IMAGES IN ARTICLE VIEW
  413. //
  414. //
  415. //
  416.  
  417. /**
  418. *
  419. * @param {Node} node
  420. * @returns {void}
  421. */
  422. function runRestoreImagesInArticleView(node) {
  423. if (!appState.restoreImagesInArticleView) {
  424. return;
  425. }
  426. /**
  427. * @type {HTMLDivElement}
  428. */
  429. // @ts-ignore
  430. const nodeElement = node;
  431.  
  432. /**
  433. * @type {HTMLDivElement | null}
  434. */
  435. const articleRoot = nodeElement?.querySelector(querySelectorPathArticleRoot);
  436. if (articleRoot) {
  437. getImageLink(articleRoot);
  438. getVideoLink(articleRoot);
  439. return;
  440. }
  441.  
  442. /**
  443. *
  444. * @param {HTMLDivElement} articleRoot
  445. */
  446. function getImageLink(articleRoot) {
  447. /**
  448. * @type {NodeListOf<HTMLAnchorElement> | null}
  449. */
  450. const ahrefElementArr = articleRoot.querySelectorAll("a[href*='t.me']:has(img[data-original-src*='cdn-telegram.org'])");
  451. const telegramPostUrl = commonGetTelegramPostUrl(node);
  452.  
  453. ahrefElementArr.forEach((ahrefElement, index) => {
  454. /**
  455. * @type {HTMLImageElement | null}
  456. */
  457. const img = ahrefElement.querySelector("img[data-original-src*='cdn-telegram.org']");
  458. if (img && telegramPostUrl) {
  459. img.onerror = function () {
  460. replaceImageSrc(img, telegramPostUrl, index);
  461. };
  462. }
  463. });
  464. }
  465.  
  466. /**
  467. *
  468. * @param {HTMLDivElement} articleRoot
  469. */
  470. function getVideoLink(articleRoot) {
  471. /**
  472. * @type {NodeListOf<HTMLVideoElement> | null}
  473. */
  474. const videos = articleRoot.querySelectorAll("video[poster*='cdn-telegram.org']");
  475. videos?.forEach((video) => {
  476. /**
  477. * @type {HTMLSourceElement | null}
  478. */
  479. const videoSource = video.querySelector("source");
  480. const telegramPostUrl = commonGetTelegramPostUrl(node);
  481. if (videoSource && telegramPostUrl) {
  482. if (checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl)) {
  483. videoSource.onerror = function () {
  484. if (checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl)) {
  485. replaceVideoSrc(videoSource, telegramPostUrl).then(() => {
  486. if (checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl)) {
  487. video.load();
  488. }
  489. });
  490. }
  491. };
  492. }
  493. }
  494. });
  495.  
  496. /**
  497. *
  498. * @param {string} telegramPostUrl
  499. * @returns
  500. */
  501. function checkIfArticleRootExistsAndHasSamePostOpened(telegramPostUrl) {
  502. if (document.querySelector(querySelectorPathArticleRoot) && commonGetTelegramPostUrl() === telegramPostUrl) {
  503. return true;
  504. }
  505. return false;
  506. }
  507. }
  508.  
  509. /**
  510. *
  511. * @param {HTMLImageElement} img
  512. * @param {string} telegramPostUrl
  513. */
  514. async function replaceImageSrc(img, telegramPostUrl, index = 0) {
  515. const doc = await commonFetchTgPostEmbed(telegramPostUrl);
  516. const imgLink = commonGetImgUrlsFromTgPost(doc);
  517. if (!imgLink) {
  518. return;
  519. }
  520. try {
  521. img.src = imgLink[index] ?? "";
  522. img.setAttribute("data-original-src", imgLink[index] ?? "");
  523. } catch (error) {
  524. console.error(`Error parsing the HTML from the telegram post. Error: ${error}`);
  525. }
  526. }
  527.  
  528. /**
  529. *
  530. * @param {HTMLSourceElement} source
  531. * @param {string} telegramPostUrl
  532. * @returns {Promise<void>}
  533. */
  534. async function replaceVideoSrc(source, telegramPostUrl) {
  535. const doc = await commonFetchTgPostEmbed(telegramPostUrl);
  536. const videoLink = commonGetVideoUrlFromTgPost(doc);
  537. try {
  538. source.src = videoLink ?? "";
  539. return Promise.resolve();
  540. } catch (error) {
  541. console.error(`Error parsing the HTML from the telegram post. Error: ${error}`);
  542. return Promise.reject(error);
  543. }
  544. }
  545. }
  546.  
  547. /**
  548. *
  549. * @param {string} telegramPostUrl
  550. * @returns {Promise<Document>}
  551. */
  552. async function commonFetchTgPostEmbed(telegramPostUrl) {
  553. // add ?embed=1 to the end of the telegramPostUrl by constructing URL object
  554. const telegramPostUrlObject = new URL(telegramPostUrl);
  555. telegramPostUrlObject.searchParams.append("embed", "1");
  556.  
  557. const requestUrl = appConfig.corsProxies[3].prefixUrl ? appConfig.corsProxies[3].prefixUrl + telegramPostUrlObject.toString() : telegramPostUrlObject;
  558. const response = await fetch(requestUrl);
  559. try {
  560. const html = await response.text();
  561. const parser = new DOMParser();
  562. const doc = parser.parseFromString(html, "text/html");
  563. return Promise.resolve(doc);
  564. } catch (error) {
  565. console.error(`Error parsing the HTML from the telegram post. Error: ${error}`);
  566. return Promise.reject(error);
  567. }
  568. }
  569.  
  570. /**
  571. *
  572. * @param {Document} doc
  573. * @returns {string[]} imageUrl
  574. */
  575. function commonGetImgUrlsFromTgPost(doc) {
  576. const imagesQuerySelectors = [
  577. ".tgme_widget_message_grouped_layer > a",
  578. "a[href^='https://t.me/'].tgme_widget_message_photo_wrap",
  579. ".tgme_widget_message_video_player[href^='https://t.me/'] > i[style*='background-image'].tgme_widget_message_video_thumb",
  580. ".tgme_widget_message_link_preview > i[style*='background-image'].link_preview_image",
  581. ];
  582.  
  583. const imgUrls = [];
  584.  
  585. for (let i = 0; i < imagesQuerySelectors.length; i++) {
  586. const images = doc.querySelectorAll(imagesQuerySelectors[i]);
  587. images.forEach((image) => {
  588. /**
  589. * @type {HTMLAnchorElement}
  590. */
  591. // @ts-ignore
  592. const element = image;
  593. const imageUrl = mediaElementParsingChooser(element);
  594. if (imageUrl) {
  595. if (!imgUrls.includes(imageUrl)) {
  596. imgUrls.push(imageUrl);
  597. }
  598. }
  599. });
  600. }
  601.  
  602. /**
  603. * @param {HTMLAnchorElement} element
  604. *
  605. * @returns {string | undefined} imageUrl
  606. */
  607. function mediaElementParsingChooser(element) {
  608. let link;
  609.  
  610. if (element.classList?.contains("tgme_widget_message_photo_wrap") && element.href?.includes("https://t.me/")) {
  611. const url = getUrlFromPhoto(element);
  612. if (url) {
  613. link = url;
  614. }
  615. } else if (element.classList?.contains("tgme_widget_message_video_thumb") && element.style.backgroundImage?.includes("cdn-telegram.org")) {
  616. const url = getUrlFromVideo(element);
  617. if (url) {
  618. link = url;
  619. }
  620. } else if (element.classList?.contains("link_preview_image") && element.style.backgroundImage?.includes("cdn-telegram.org")) {
  621. const url = getUrlFromLinkPreview(element);
  622. if (url) {
  623. link = url;
  624. }
  625. }
  626.  
  627. return link;
  628. }
  629.  
  630. /**
  631. *
  632. * @param {HTMLAnchorElement} element
  633. * @returns {string | undefined}
  634. */
  635. function getUrlFromPhoto(element) {
  636. const backgroundImageUrl = element?.style.backgroundImage;
  637. return commonGetUrlFromBackgroundImage(backgroundImageUrl);
  638. }
  639.  
  640. /**
  641. *
  642. * @param {HTMLAnchorElement} element
  643. * @returns {string | undefined}
  644. */
  645. function getUrlFromVideo(element) {
  646. const backgroundImageUrl = element?.style.backgroundImage;
  647. return commonGetUrlFromBackgroundImage(backgroundImageUrl || "");
  648. }
  649.  
  650. /**
  651. *
  652. * @param {HTMLElement} element
  653. * @returns
  654. */
  655. function getUrlFromLinkPreview(element) {
  656. const backgroundImageUrl = element?.style.backgroundImage;
  657. return commonGetUrlFromBackgroundImage(backgroundImageUrl);
  658. }
  659.  
  660. return imgUrls;
  661. }
  662.  
  663. /**
  664. *
  665. * @param {string} backgroundImageUrl
  666. * @returns {string | undefined}
  667. */
  668. function commonGetUrlFromBackgroundImage(backgroundImageUrl) {
  669. /**
  670. * @type {string | undefined}
  671. */
  672. let imageUrl;
  673. try {
  674. imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1];
  675. } catch (error) {
  676. imageUrl = backgroundImageUrl?.slice(5, -2);
  677. }
  678.  
  679. if (!imageUrl || imageUrl == "undefined") {
  680. return;
  681. }
  682.  
  683. if (!imageUrl?.startsWith("http")) {
  684. console.error(`The image could not be parsed. Image URL: ${imageUrl}`);
  685. return;
  686. }
  687. return imageUrl;
  688. }
  689.  
  690. /**
  691. *
  692. * @param {Document} doc
  693. * @returns {string | undefined} imageUrl
  694. */
  695. function commonGetVideoUrlFromTgPost(doc) {
  696. /**
  697. * @type {HTMLVideoElement | null}
  698. */
  699. const video = doc.querySelector("video[src*='cdn-telegram.org']");
  700. const videoUrl = video?.src;
  701. return videoUrl;
  702. }
  703.  
  704. /**
  705. *
  706. * @param {Node | undefined} node
  707. * @returns {string}
  708. */
  709. function commonGetTelegramPostUrl(node = undefined) {
  710. return getFromArticleView() ?? getFromNode(node) ?? "";
  711.  
  712. /**
  713. *
  714. * @returns {string | undefined}
  715. */
  716. function getFromArticleView() {
  717. /**
  718. * @type {HTMLAnchorElement | null}
  719. */
  720. const element = document.querySelector(".article_title > a[href^='https://t.me/']");
  721. return element?.href;
  722. }
  723.  
  724. /**
  725. *
  726. * @param {Node | undefined} node
  727. * @returns {string}
  728. */
  729. function getFromNode(node) {
  730. /**
  731. * @type {HTMLDivElement}
  732. */
  733. // @ts-ignore
  734. const nodeElement = node;
  735. /**
  736. * @type {HTMLAnchorElement | null}
  737. */
  738. const ahrefElement = nodeElement.querySelector("a[href*='t.me']");
  739. const telegramPostUrl = ahrefElement?.href ?? "";
  740. // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it
  741. try {
  742. return new URL(telegramPostUrl).origin + new URL(telegramPostUrl).pathname;
  743. } catch (error) {
  744. return telegramPostUrl?.split("?")[0];
  745. }
  746. }
  747. }
  748.  
  749. // Create an observer instance linked to the callback function
  750. const tmObserverImageRestore = new MutationObserver(callback);
  751.  
  752. // Start observing the target node for configured mutations
  753. tmObserverImageRestore.observe(targetNode, mutationObserverGlobalConfig);
  754.  
  755. registerCommands();
  756. })();