Копирование из защищённого канала Телеграм

Оказалось, что в веб-версии Телеграма модератор канала может запретить копирование текста сообщений. Тривиальные способы, без привлечения браузерного “Inspect element”, не работают.

Скриншот выпадающего меню в Телеграм канале без возможности копирования

К счастью, для браузера Firefox есть дополнение GreaseMonkey. Наверняка, для Google Chrome тоже что-то подобное есть.

Скриншот выпадающего меню дополнения GreaseMonkey

Подготовлен скрипт, который добавляет кнопку “Copy” рядом с каждым сообщением, после включения в GreaseMonkey.

Скриншот выпадающего меню GreaseMonkey после включения добавленного скрипта
Скриншот сообщения в Телеграм с новой кнопкой ‘Copy’
Выглядит топорно, но и задача примитивная.

Скрипт в свёрнутом блоке ниже:

// ==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 });
})();