Greasy Fork 支持简体中文。

Голосовые сообщения для лолза

Добавляет кнопку записи гс на страницах создания тем и в других местах

// ==UserScript==
// @name         Голосовые сообщения для лолза
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Добавляет кнопку записи гс на страницах создания тем и в других местах
// @author       eretly
// @match        https://zelenka.guru/*
// @match        https://lolz.guru/*
// @match        https://lolz.live/*
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_getUserMedia
// ==/UserScript==

(function() {
    'use strict';

    const CHAT_ID = '-'; // Замените на айди вашего канала
    const BOT_TOKEN = ''; // Замените на токен тг бота
    const ENABLE_TRANSCRIPTION = true; // Переключатель для функции транскрипции

    const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
    recognition.lang = 'ru-RU';
    recognition.continuous = true;
    recognition.interimResults = false;

    let mediaRecorder;
    let audioChunks = [];
    let isRecording = false;
    let transcriptText = '';

    recognition.onresult = function(event) {
        if (ENABLE_TRANSCRIPTION) {
            const last = event.results.length - 1;
            transcriptText += event.results[last][0].transcript + ' ';
        }
    };

    recognition.onerror = function(event) {
        console.error('Ошибка распознавания речи:', event.error);
    };

    function writeString(view, offset, string) {
        for (let i = 0; i < string.length; i++) {
            view.setUint8(offset + i, string.charCodeAt(i));
        }
    }

    function bufferToWav(audioBuffer) {
        const numOfChannels = audioBuffer.numberOfChannels;
        const sampleRate = audioBuffer.sampleRate;
        const samples = audioBuffer.getChannelData(0);
        const buffer = new ArrayBuffer(44 + samples.length * 2);
        const view = new DataView(buffer);

        writeString(view, 0, 'RIFF');
        view.setUint32(4, 36 + samples.length * 2, true);
        writeString(view, 8, 'WAVE');
        writeString(view, 12, 'fmt ');

        view.setUint32(16, 16, true);
        view.setUint16(20, 1, true);
        view.setUint16(22, numOfChannels, true);
        view.setUint32(24, sampleRate, true);
        view.setUint32(28, sampleRate * numOfChannels * 2, true);
        view.setUint16(32, numOfChannels * 2, true);
        view.setUint16(34, 16, true);
        writeString(view, 36, 'data');
        view.setUint32(40, samples.length * 2, true);

        let offset = 44;
        for (let i = 0; i < samples.length; i++) {
            view.setInt16(offset, samples[i] * 0x7FFF, true);
            offset += 2;
        }

        return new Blob([view], { type: 'audio/wav' });
    }

    GM_addStyle(`
    .lzt-fe-se-extraButton[data-cmd="voiceRecord"] {
        width: 24px;
        height: 24px;
        padding: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        cursor: pointer;
    }
    .lzt-fe-se-extraButton[data-cmd="voiceRecord"] svg {
        width: 20px;
        height: 20px;
        stroke: #8c8c8c;
        transition: stroke 0.3s ease;
    }
    .lzt-fe-se-extraButton[data-cmd="voiceRecord"]:hover svg {
        stroke: #d6d6d6;
    }
    .lzt-fe-se-extraButton[data-cmd="voiceRecord"].recording svg {
        stroke: #cc0000;
    }
    .fr-command[data-cmd="voiceRecord"] {
        position: relative;
    }
    .fr-command[data-cmd="voiceRecord"].recording i {
        color: #cc0000;
        animation: pulse 1.5s infinite;
    }
    @keyframes pulse {
        0% { opacity: 1; }
        50% { opacity: 0.5; }с
        100% { opacity: 1; }
    }
`);

    function createRecordButton(isToolbarButton = false) {
        const recordButton = document.createElement(isToolbarButton ? 'button' : 'div');
        recordButton.type = 'button';
        recordButton.tabIndex = -1;
        recordButton.setAttribute('role', 'button');
        recordButton.setAttribute('aria-disabled', 'false');
        recordButton.className = isToolbarButton ? 'fr-command fr-btn' : 'lzt-fe-se-extraButton';
        recordButton.setAttribute('data-cmd', 'voiceRecord');
        recordButton.title = 'Начать / Остановить Запись голоса';

        if (isToolbarButton) {
            recordButton.innerHTML = `
            <i class="fal fa-microphone" aria-hidden="true"></i>
            <span class="fr-sr-only">Запись голоса</span>
        `;
        } else {
            recordButton.innerHTML = `
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M19 10V12C19 15.866 15.866 19 12 19M5 10V12C5 15.866 8.13401 19 12 19M12 19V22M8 22H16M12 15C10.3431 15 9 13.6569 9 12V5C9 3.34315 10.3431 2 12 2C13.6569 2 15 3.34315 15 5V12C15 13.6569 13.6569 15 12 15Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
        `;
        }

        recordButton.addEventListener('click', function(e) {
            e.preventDefault();
            e.stopPropagation();
            console.log('Кнопка нажата (прямой обработчик)');
            handleRecordButtonClick(this);
        });

        return recordButton;
    }


    function addRecordButton() {
        const buttonContainers = [
            { container: document.querySelector('.lzt-fe-se-extraButtonsContainer'), isToolbar: false },
            { container: document.querySelector('.js-lzt-fe-extraButtons'), isToolbar: false },
            { container: document.querySelector('.fr-toolbar .fr-btn-grp.fr-float-left'), isToolbar: true }
        ];

        buttonContainers.forEach(({ container, isToolbar }) => {
            if (container) {
                const existingButton = container.querySelector('[data-cmd="voiceRecord"]');
                if (!existingButton) {
                    const recordButton = createRecordButton(isToolbar);
                    container.appendChild(recordButton);
                    console.log('Кнопка записи добавлена в контейнер', container);
                }
            }
        });
    }

    function handleRecordButtonClick(button) {
        if (!isRecording) {
            startRecording(button);
        } else {
            stopRecording(button);
        }
    }

    async function startRecording(button) {
        if (!button || !button.classList) {
            return;
        }

        try {
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

            if (mediaRecorder && mediaRecorder.state === 'recording') {
                console.warn('MediaRecorder уже записывает');
                return;
            }

            mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
            audioChunks = [];
            transcriptText = '';

            mediaRecorder.addEventListener('dataavailable', event => {
                audioChunks.push(event.data);
            });

            mediaRecorder.addEventListener('stop', () => {
                const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
                convertToOgg(audioBlob, button);
                stream.getTracks().forEach(track => track.stop());
                if (ENABLE_TRANSCRIPTION) {
                    recognition.stop();
                }
            });

            setTimeout(() => {
                mediaRecorder.start();
                if (ENABLE_TRANSCRIPTION) {
                    recognition.start();
                }
                isRecording = true;
                button.classList.add('recording');
            }, 10);

        } catch (err) {
            console.error('Ошибка при получении доступа к микрофону:', err);
            alert('Не удалось получить доступ к микрофону: ' + err.message);
        }
    }

    function stopRecording(button) {
        if (!button || !button.classList) {
            return;
        }

        if (!mediaRecorder || mediaRecorder.state === 'inactive' || mediaRecorder.state === 'paused') {
            console.warn('Запись уже остановлена или не была запущена.');
            return;
        }

        setTimeout(() => {
            mediaRecorder.stop();
            isRecording = false;
            button.classList.remove('recording');
            console.log('Запись остановлена');
        }, 10);
    }


    function convertToOgg(audioBlob, button) {
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const reader = new FileReader();
        reader.onload = async function() {
            const audioBuffer = await audioContext.decodeAudioData(reader.result);
            const duration = audioBuffer.duration;
            const offlineContext = new OfflineAudioContext(1, audioBuffer.length, audioBuffer.sampleRate);
            const source = offlineContext.createBufferSource();
            source.buffer = audioBuffer;
            source.connect(offlineContext.destination);
            source.start();
            offlineContext.startRendering().then(function(renderedBuffer) {
                const wavBlob = bufferToWav(renderedBuffer);
                const oggBlob = convertWavToOgg(wavBlob);
                sendAudioToTelegram(oggBlob, duration, button);
            });
        };
        reader.readAsArrayBuffer(audioBlob);
    }

    function convertWavToOgg(wavBlob) {
        return wavBlob;
    }

    function sendAudioToTelegram(audioBlob, duration, button) {
        const formData = new FormData();
        formData.append('chat_id', CHAT_ID);
        formData.append('voice', audioBlob, 'voice.mp3');
        formData.append('duration', Math.round(duration));

        const xhr = new XMLHttpRequest();
        xhr.open('POST', `https://api.telegram.org/bot${BOT_TOKEN}/sendVoice`, true);

        xhr.onload = function() {
            if (xhr.status === 200) {
                const responseData = JSON.parse(xhr.responseText);
                if (responseData.ok && responseData.result && responseData.result.message_id) {
                    const telegramPostLink = `https://t.me/${responseData.result.chat.username}/${responseData.result.message_id}`;
                    insertTextAndLinkIntoInput(button, transcriptText, telegramPostLink);
                } else {
                    alert('Не удалось получить ссылку на пост в Telegram');
                }
            } else {
                alert('Ошибка при отправке аудио в Telegram');
            }
        };

        xhr.onerror = function() {
            alert('Ошибка при отправке аудио в Telegram');
        };

        xhr.send(formData);
    }

    function insertTextAndLinkIntoInput(button, transcription, telegramLink) {
        const inputElement = button.closest('.fr-wrapper')?.querySelector('.fr-element.fr-view[contenteditable="true"]') ||
              document.querySelector('.fr-element.fr-view');

        if (inputElement) {
            const linkElement = document.createElement('a');
            linkElement.href = telegramLink;
            linkElement.target = '_blank';
            linkElement.textContent = telegramLink;
            inputElement.appendChild(linkElement);

            inputElement.appendChild(document.createElement('br'));

            if (ENABLE_TRANSCRIPTION && transcription.trim()) {
                const transcriptionText = document.createTextNode(`Транскрипция: [${transcription.trim()}]`);
                inputElement.appendChild(transcriptionText);
                inputElement.appendChild(document.createElement('br'));
            }

            const inputEvent = new Event('input', { bubbles: true });
            inputElement.dispatchEvent(inputEvent);

            inputElement.scrollTop = inputElement.scrollHeight;
        } else {
            console.error('Не удалось найти поле ввода');
        }
    }

    addRecordButton();

    document.addEventListener('click', function(e) {
        if (e.target && e.target.closest('.lzt-fe-se-extraButton[data-cmd="voiceRecord"]')) {
            console.log('Кнопка нажата через делегирование');
            handleRecordButtonClick(e);
        }
    });

    function init() {
        addRecordButton();

        document.addEventListener('click', function(e) {
            const recordButton = e.target.closest('[data-cmd="voiceRecord"]');
            if (recordButton) {
                console.log('Кнопка нажата (делегирование)');
                handleRecordButtonClick(recordButton);
            }
        });

        recognition.onresult = function(event) {
            if (ENABLE_TRANSCRIPTION) {
                const last = event.results.length - 1;
                transcriptText += event.results[last][0].transcript + ' ';
            }
        };

        recognition.onerror = function(event) {
            console.error('Ошибка распознавания речи:', event.error);
        };
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    const observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.type === 'childList') {
                addRecordButton();
            }
        });
    });

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