Telegram Web - Allow Saving Content

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

Verzia zo dňa 22.10.2023. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Telegram Web - Allow Saving Content
// @namespace    c0d3r
// @license      MIT
// @version      0.3
// @description  Bypass Telegram's saving content restrictions for media and text; batch download media from selected messages
// @author       c0d3r
// @match        https://web.telegram.org/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=telegram.org
// @grant        unsafeWindow
// @grant        GM_addStyle
// ==/UserScript==

// Extract media from message and download to disc
function downloadMediaFromMessage(msg) {
    var myMedia;

    if (msg.media) {
        // Extract the media object; simple alternative to getMediaFromMessage
        myMedia = msg.media.document || msg.media.photo;
    }

    if (myMedia) {
        // Download media using the built-in function; auto sets file name and extension
        unsafeWindow.appDownloadManager.downloadToDisc({media: myMedia});
    }
}

// Throttle download of multiple medias by 1 second
function slowDown(secs, msg, btnElm, btnTxt, btnIco) {
    setTimeout(function () {
        btnElm.disabled = true;
        btnElm.style.opacity = 0.6;
        btnTxt.textContent = '..' + (secs + 1) + '..';
        btnIco.innerHTML = '&#xe9b8';

        downloadMediaFromMessage(msg);
    }, secs * 1000);
}

// Get message object then download
async function downloadSingleMedia(pid, mid) {
    // Get the message object based on peer and message ID
    var msg = await unsafeWindow.mtprotoMessagePort.getMessageByPeer(pid, mid);

    downloadMediaFromMessage(msg);
}

// Download multiple medias from selected messages
async function downloadSelectedMedia() {
    var msgs = await unsafeWindow.appImManager.chat.selection.getSelectedMessages();
    var secs = 0;

    var btnElm = document.querySelector('#batch-btn');
    var btnTxt = btnElm.querySelector('.i18n');
    var btnIco = btnElm.querySelector('.tgico');

    msgs.forEach(function (msg, ind) {
        // Only process messages with media
        if (msg.media && (msg.media.document || msg.media.photo)) {
            slowDown(secs, msg, btnElm, btnTxt, btnIco);
            secs++;
        }

        // Reset the batch button after last download
        if (ind === msgs.length - 1) {
            setTimeout(function () {
                btnElm.disabled = false;
                btnElm.style.opacity = 1;
                btnTxt.textContent = 'D/L';
                btnIco.innerHTML = '';
            }, secs * 1000);
        }
    });
}

(function () {
    'use strict';

    if (window.location.pathname.startsWith('/a/')) {
        // Redirect to the WebK version from the WebA version
        window.location.replace(window.location.href.replace('.org/a/', '.org/k/'));
    } else {
        // The root element used for watching and listening
        var colCenter = document.querySelector('#column-center');

        // Array of class names for media; we only add Download button if these are right clicked
        var clArray = ['photo', 'audio', 'video', 'voice-message', 'media-round', 'grouped-item', 'document-container', 'sticker'];

        // HTML code for the Download button
        var btnHtml = '<div class="btn-menu-item rp-overflow" id="down-btn"><span class="tgico btn-menu-item-icon">&#xe93e;</span><span class="i18n btn-menu-item-text">Download</span></div>';

        // HTML code for the batch D/L button
        var batchBtnHtml = '&nbsp;&nbsp;<button class="btn-primary btn-transparent text-bold" id="batch-btn" title="Download Media"><span class="tgico">&#xe93e;</span>&nbsp;<span class="i18n">D/L</span></button>';

        // A flag for checking if we need to add the Download button
        var needBtn = false;

        // Variables for the current message and peer ID
        var curMid, curPid, observer;

        // Add CSS styles to allow text selection
        GM_addStyle('.chat.no-forwards .bubbles, .bubble, .bubble-content { -webkit-user-select: text!important; -moz-user-select: text!important; user-select: text!important; }');

        colCenter.addEventListener('mouseup', function (e) {
            // Listen to the right mouse button clicks
            if (e.button === 2) {
                needBtn = false;
                // Test if the current chat has restricted content saving
                if (document.querySelector('.chat.no-forwards')) {
                    // Find the closest element containing message and peer IDs
                    var closest = e.target.closest('[data-mid]');
                    if (closest) {
                        // Check if the element actually contains some media classes
                        if (clArray.some(function (clName) {
                            return closest.classList.contains(clName);
                        })) {
                            curMid = closest.dataset.mid;
                            curPid = closest.dataset.peerId;
                            needBtn = true;
                        }
                    }
                }
            }
        });

        observer = new MutationObserver(function (mutList) {
            mutList.forEach(function (mut) {
                mut.addedNodes.forEach(function (anod) {
                    // Check if context menu has been added to the DOM
                    if (anod.id === 'bubble-contextmenu' && needBtn) {
                        // Add the custom Download button and assign a click event
                        anod.querySelector('.btn-menu-item').insertAdjacentHTML('beforebegin', btnHtml);
                        anod.querySelector('#down-btn').addEventListener('click', function () {
                            downloadSingleMedia(curPid, curMid);
                        });
                    }

                    // Check if selection popup has been added to the DOM
                    if (anod.classList && anod.classList.contains('selection-wrapper')) {
                        anod.querySelector('.selection-container-left').insertAdjacentHTML('beforeend', batchBtnHtml);
                        anod.querySelector('#batch-btn').addEventListener('click', function () {
                            downloadSelectedMedia();
                        });
                    }
                });
            });
        });

        // Observe when context menu is added to the DOM
        observer.observe(colCenter, {
            subtree: true, childList: true
        });
    }
})();