Telegram Web - Allow Saving Content

Bypass Telegram's saving content restrictions for media and text; batch download media from selected messages

  1. // ==UserScript==
  2. // @name Telegram Web - Allow Saving Content
  3. // @namespace c0d3r
  4. // @license MIT
  5. // @version 0.5
  6. // @description Bypass Telegram's saving content restrictions for media and text; batch download media from selected messages
  7. // @author c0d3r
  8. // @match https://web.telegram.org/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=telegram.org
  10. // @grant unsafeWindow
  11. // @grant GM_addStyle
  12. // ==/UserScript==
  13.  
  14. // Extract media from message and download to disc
  15. function downloadMediaFromMessage(msg) {
  16. var myMedia;
  17.  
  18. if (msg.media) {
  19. // Extract the media object; simple alternative to getMediaFromMessage
  20. myMedia = msg.media.document || msg.media.photo;
  21. }
  22.  
  23. if (myMedia) {
  24. // Download media using the built-in function; auto sets file name and extension
  25. unsafeWindow.appDownloadManager.downloadToDisc({media: myMedia});
  26. }
  27. }
  28.  
  29. // Throttle download of multiple medias by 1 second
  30. function slowDown(secs, msg, btnElm, btnTxt, btnIco) {
  31. setTimeout(function () {
  32. btnElm.disabled = true;
  33. btnElm.style.opacity = 0.6;
  34. btnTxt.textContent = '..' + (secs + 1) + '..';
  35. btnIco.textContent = '🕔';
  36.  
  37. downloadMediaFromMessage(msg);
  38. }, secs * 1000);
  39. }
  40.  
  41. // Get message object then download
  42. async function downloadSingleMedia(pid, mid) {
  43. // Get the message object based on peer and message ID
  44. var msg = await unsafeWindow.mtprotoMessagePort.getMessageByPeer(pid, mid);
  45.  
  46. downloadMediaFromMessage(msg);
  47. }
  48.  
  49. // Download multiple medias from selected messages
  50. async function downloadSelectedMedia() {
  51. var msgs = await unsafeWindow.appImManager.chat.selection.getSelectedMessages();
  52. var secs = 0;
  53.  
  54. var btnElm = document.querySelector('#batch-btn');
  55. var btnTxt = btnElm.querySelector('.i18n');
  56. var btnIco = btnElm.querySelector('.mytgico');
  57.  
  58. msgs.forEach(function (msg, ind) {
  59. // Only process messages with media
  60. if (msg.media && (msg.media.document || msg.media.photo)) {
  61. slowDown(secs, msg, btnElm, btnTxt, btnIco);
  62. secs++;
  63. }
  64.  
  65. // Reset the batch button after last download
  66. if (ind === msgs.length - 1) {
  67. setTimeout(function () {
  68. btnElm.disabled = false;
  69. btnElm.style.opacity = 1;
  70. btnTxt.textContent = 'D/L';
  71. btnIco.textContent = '📥';
  72. }, secs * 1000);
  73. }
  74. });
  75. }
  76.  
  77. (function () {
  78. 'use strict';
  79.  
  80. if (window.location.pathname.startsWith('/a/')) {
  81. // Redirect to the WebK version from the WebA version
  82. window.location.replace(window.location.href.replace('.org/a/', '.org/k/'));
  83. } else {
  84. // The root element used for watching and listening
  85. var colCenter = document.querySelector('#column-center');
  86.  
  87. // Array of class names for media; we only add Download button if these are right clicked
  88. var clArray = ['photo', 'audio', 'video', 'voice-message', 'media-round', 'grouped-item', 'document-container', 'sticker'];
  89.  
  90. // HTML code for the Download button
  91. var btnHtml = '<div class="btn-menu-item rp-overflow" id="down-btn"><span class="mytgico btn-menu-item-icon" style="font-size: 16px;">📥</span><span class="i18n btn-menu-item-text">Download</span></div>';
  92.  
  93. // HTML code for the batch D/L button
  94. var batchBtnHtml = '&nbsp;&nbsp;<button class="btn-primary btn-transparent text-bold" id="batch-btn" title="Download Media"><span class="mytgico" style="padding-bottom: 2px;">📥</span>&nbsp;<span class="i18n">D/L</span></button>';
  95.  
  96. // A flag for checking if we need to add the Download button
  97. var needBtn = false;
  98.  
  99. // Variables for the current message and peer ID
  100. var curMid, curPid, observer;
  101.  
  102. // Add CSS styles to allow text selection
  103. GM_addStyle('.no-forwards .bubbles, .bubble, .bubble-content { -webkit-user-select: text!important; -moz-user-select: text!important; user-select: text!important; }');
  104.  
  105. // Unlock Ctrl+C to copy selected text
  106. var origListener = EventTarget.prototype.addEventListener;
  107. EventTarget.prototype.addEventListener = function(type) {
  108. if (type !== 'copy') {
  109. origListener.apply(this, arguments);
  110. }
  111. };
  112.  
  113. colCenter.addEventListener('mouseup', function (e) {
  114. // Listen to the right mouse button clicks
  115. if (e.button === 2) {
  116. needBtn = false;
  117. // Test if the current chat has restricted content saving
  118. if (document.querySelector('.no-forwards')) {
  119. // Find the closest element containing message and peer IDs
  120. var closest = e.target.closest('[data-mid]');
  121. if (closest) {
  122. // Check if the element actually contains some media classes
  123. if (clArray.some(function (clName) {
  124. return closest.classList.contains(clName);
  125. })) {
  126. curMid = closest.dataset.mid;
  127. curPid = closest.dataset.peerId;
  128. needBtn = true;
  129. }
  130. }
  131. }
  132. }
  133. });
  134.  
  135. observer = new MutationObserver(function (mutList) {
  136. mutList.forEach(function (mut) {
  137. mut.addedNodes.forEach(function (anod) {
  138. // Check if context menu has been added to the DOM
  139. if (anod.id === 'bubble-contextmenu' && needBtn) {
  140. // Add the custom Download button and assign a click event
  141. anod.querySelector('.btn-menu-item').insertAdjacentHTML('beforebegin', btnHtml);
  142. anod.querySelector('#down-btn').addEventListener('click', function () {
  143. downloadSingleMedia(curPid, curMid);
  144. });
  145. }
  146.  
  147. // Check if selection popup has been added to the DOM
  148. if (anod.classList && anod.classList.contains('selection-wrapper')) {
  149. anod.querySelector('.selection-container-left').insertAdjacentHTML('beforeend', batchBtnHtml);
  150. anod.querySelector('#batch-btn').addEventListener('click', function () {
  151. downloadSelectedMedia();
  152. });
  153. }
  154. });
  155. });
  156. });
  157.  
  158. // Observe when context menu is added to the DOM
  159. observer.observe(colCenter, {
  160. subtree: true, childList: true
  161. });
  162. }
  163. })();