Copying from protected Telegram channels

It turns out that Telegram’s web version allows channel moderators to disable copying of messages. Simple methods, without using browser “Inspect element,” do not work.

Screenshot of a drop-down menu in Telegram channel with copy protection

Fortunately, Firefox has a useful addon called GreaseMonkey. There’s probably something similar available for Google Chrome as well.

Screenshot of a drop-down menu of GreaseMonkey add-on

A script has been prepared that adds a “Copy” button next to each message after enabling it in GreaseMonkey.

Screenshot of a drop-down menu of GreaseMonkey after enabling the newly added script
Screenshot of a Telegram message with a new ‘Copy’ button
Looks crude, but the task is pretty simple, too

The script is in the collapsed block below:

// ==UserScript==
// @name         Copy Content Blocks
// @namespace    https://example.com
// @version      1.0
// @description  Finds .content-inner blocks, adds a Copy button, and copies their text.
// @match        https://web.telegram.org/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Copy text to clipboard with a fallback.
     */
    function copyTextToClipboard(text) {
        if (navigator.clipboard && navigator.clipboard.writeText) {
            // Modern async clipboard API
            return navigator.clipboard.writeText(text);
        } else {
            // Fallback for older browsers
            return new Promise((resolve, reject) => {
                const temp = document.createElement('textarea');
                temp.value = text;
                document.body.appendChild(temp);
                temp.select();

                const success = document.execCommand('copy');
                document.body.removeChild(temp);

                success ? resolve() : reject(new Error('execCommand: copy failed.'));
            });
        }
    }

    /**
     * Attach a "Copy" button to all .Message blocks that haven't been processed yet.
     */
    function addCopyButtons() {
        const messageBlocks = document.querySelectorAll('.Message:not([data-copybtn-attached])');

        messageBlocks.forEach(block => {
            block.setAttribute('data-copybtn-attached', 'true');

            // Ensure the block is positioned so the absolutely placed button stays at the top-right corner
            block.style.position = 'relative';

            // Create the copy button
            const copyBtn = document.createElement('button');
            copyBtn.innerText = 'Copy';

            // Style the button to float at the top-right, above other elements
            copyBtn.style.zIndex = '999999';
            copyBtn.style.pointerEvents = 'auto';
            copyBtn.style.cursor = 'pointer';

            // Clicking the button copies text content of the block
            copyBtn.addEventListener('click', () => {
                const textToCopy = block.textContent.trim();
                copyTextToClipboard(textToCopy)
                    .then(() => {
                        console.log('Copied block content');
                    })
                    .catch(err => {
                        console.error('Failed to copy text:', err);
                    });
            });

            // Add the button to the block
            block.prepend(copyBtn);
        });
    }

    // Run once immediately
    addCopyButtons();

    // Use a MutationObserver for dynamically loaded content
    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                addCopyButtons();
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();