Moodle AutoPilot

Полный набор для 100% посещаемости дистанционных лекций

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Moodle AutoPilot
// @namespace    https://t.me/johannmosin
// @version      1.0.5
// @description  Полный набор для 100% посещаемости дистанционных лекций
// @author       Johann Mosin
// @match        https://edu.vsu.ru/mod/bigbluebuttonbn/view.php*
// @match        https://*.edu.vsu.ru/html5client/*
// @match        https://www.cs.vsu.ru/brs/att_marks_report_student/*
// @match        https://edu.vsu.ru/mod/attendance/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// @icon         https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Glagolitic_ljudi.svg/47px-Glagolitic_ljudi.svg.png
// ==/UserScript==

(function() {
    'use strict';

    // --- Settings Keys ---
    const SETTINGS_KEYS = {
        autoConnect: 'autoConnectEnabled',
        autoHello: 'autoHelloEnabled',
        autoLeave: 'autoLeaveEnabled',
        autoBRS: 'autobrsEnabled',
        autoAttendance: 'autoAttendanceEnabled'
    };

    // --- Styles ---
    GM_addStyle(`
        .moodle-autotool-button {
            cursor: pointer;
            border-radius: 7px;
            padding: 8px 15px !important;
            transition: background 0.3s;
            color: black !important;
            border: none !important;
            margin: 5px;
            text-align: center;
            font-size: 1rem;
            line-height: 1.5;
        }
        .moodle-autotool-button:hover {
            opacity: 0.9;
        }
        .moodle-autotool-button.off {
            background: rgba(255, 193, 7, 0.25);
        }
        .moodle-autotool-button.on {
            background: rgba(0, 128, 0, 0.25) !important;
        }

        .autoTool-controls {
            display: flex;
            margin: 10px 0;
            flex-wrap: wrap;
        }
        .autoTool-button {
             width: 180px;
        }

        #toggleBRS {
            width: 150px;
            margin-bottom: 2px;
        }

        #toggleAttendance {
             margin-top: 2px;
             margin-bottom: 2px;
        }

        .moodle-autotool-nav-item {
            display: flex;
            align-items: center;
        }
    `);

    function createToggleButton(id, textPrefix, settingKey, initialState = false, onClickCallback = null) {
        const button = document.createElement('button');
        button.id = id;
        button.className = `moodle-autotool-button ${id === 'autoConnectBtn' || id === 'autoHelloBtn' || id === 'autoLeaveBtn' ? 'autoTool-button' : ''}`;
        button.dataset.settingKey = settingKey;

        const updateButtonState = (btn, enabled) => {
            btn.textContent = `${textPrefix}: ${enabled ? 'ВКЛ' : 'ВЫКЛ'}`;
            if (enabled) {
                btn.classList.remove('off');
                btn.classList.add('on');
            } else {
                btn.classList.remove('on');
                btn.classList.add('off');
            }
        };

        let isEnabled = GM_getValue(settingKey, initialState);
        updateButtonState(button, isEnabled);

        button.addEventListener('click', (e) => {
            e.preventDefault();
            isEnabled = !isEnabled;
            GM_setValue(settingKey, isEnabled);
            updateButtonState(button, isEnabled);
            if (onClickCallback) {
                onClickCallback(isEnabled);
            }
        });

        return button;
    }

    const AutoTools = {
        settings: {
            autoConnect: GM_getValue(SETTINGS_KEYS.autoConnect, false),
            autoHello: GM_getValue(SETTINGS_KEYS.autoHello, false),
            autoLeave: GM_getValue(SETTINGS_KEYS.autoLeave, false)
        },
        intervals: {
            connect: null,
            hello: null,
            leave: null,
            bbbButtonCheck: null
        },
        timeouts: {
            reload: null
        },
        flags: {
            connectCheckStarted: false,
            helloMessageSent: false
        },

        initUI(isConnectPage, isConferencePage) {
            if (document.querySelector('.autoTool-controls')) return;

            const controlPanel = document.createElement('div');
            controlPanel.className = 'autoTool-controls';

            const createAndAppend = (id, text, key, callback) => {
                const btn = createToggleButton(id, text, key, this.settings[key.replace('Enabled','')], callback);
                controlPanel.appendChild(btn);
            };

            if (isConnectPage) {
                createAndAppend('autoConnectBtn', 'AutoConnect', SETTINGS_KEYS.autoConnect, (enabled) => {
                    this.settings.autoConnect = enabled;
                    enabled ? this.startAutoConnect() : this.stopAutoConnect();
                });
            }

            if (isConnectPage || isConferencePage) {
                createAndAppend('autoHelloBtn', 'AutoHello', SETTINGS_KEYS.autoHello, (enabled) => {
                    this.settings.autoHello = enabled;
                    enabled ? this.startAutoHello() : this.stopAutoHello();
                });
                createAndAppend('autoLeaveBtn', 'AutoLeave', SETTINGS_KEYS.autoLeave, (enabled) => {
                    this.settings.autoLeave = enabled;
                    enabled ? this.startAutoLeave() : this.stopAutoLeave();
                });
            }

            if (controlPanel.hasChildNodes()) {
                if (isConnectPage) {
                    const targetElement = document.querySelector('[class*="custom-select"]') || document.querySelector('#region-main') || document.body;
                    if(targetElement === document.body) targetElement.insertBefore(controlPanel, targetElement.firstChild);
                    else targetElement.parentNode.insertBefore(controlPanel, targetElement.nextSibling);
                } else if (isConferencePage) {
                    const userListContent = document.querySelector('[data-test="userList"]');
                    const chatInputArea = document.querySelector('#message-input')?.parentNode;
                    const targetParent = userListContent?.parentNode || chatInputArea || document.body;
                    const referenceNode = userListContent || (chatInputArea ? chatInputArea.firstChild : null) || document.body.firstChild;

                    const observer = new MutationObserver((mutations, obs) => {
                        let inserted = false;
                        const userList = document.querySelector('[data-test="userList"]');
                        const chatInput = document.querySelector('#message-input')?.parentNode;
                        if (userList) {
                            userList.parentNode.insertBefore(controlPanel, userList);
                            inserted = true;
                        } else if (chatInput) {
                            chatInput.parentNode.insertBefore(controlPanel, chatInput);
                            inserted = true;
                        }
                        if (inserted) {
                            obs.disconnect();
                        }
                    });

                    if (userListContent) {
                        userListContent.parentNode.insertBefore(controlPanel, userListContent);
                    } else if (chatInputArea) {
                        chatInputArea.parentNode.insertBefore(controlPanel, chatInputArea);
                    } else {
                        observer.observe(document.body, { childList: true, subtree: true });
                        setTimeout(() => {
                            observer.disconnect();
                            if (!document.querySelector('.autoTool-controls')) {
                                document.body.insertBefore(controlPanel, document.body.firstChild);
                            }
                        }, 15000);
                    }
                }
            }
        },

        startAutoConnect() {
            if (!this.flags.connectCheckStarted) {
                this.flags.connectCheckStarted = true;
                this.timeouts.reload = setTimeout(() => {
                    this.checkForSessionLink();
                }, 10000);
            }
        },
        stopAutoConnect() {
            clearInterval(this.intervals.connect);
            clearTimeout(this.timeouts.reload);
            this.intervals.connect = null;
            this.timeouts.reload = null;
            this.flags.connectCheckStarted = false;
        },
        resetReloadTimeout() {
            clearTimeout(this.timeouts.reload);
            this.timeouts.reload = setTimeout(() => {
                if (this.settings.autoConnect) {
                    location.reload();
                }
            }, 10000);
        },
        checkForSessionLink() {
            const sessionLink = Array.from(document.querySelectorAll('a')).find(a =>
                                                                                a.textContent.includes("Подключиться к сеансу"));

            if (sessionLink && sessionLink.href) {
                window.open(sessionLink.href, '_blank');
                this.stopAutoConnect();
            } else {
                this.resetReloadTimeout();
            }
        },

        handleHtml5ClientPage() {
            this.intervals.bbbButtonCheck = setInterval(() => {
                const joinButton = document.querySelector('button[aria-label="Только слушать"]');
                if (joinButton) {
                    joinButton.click();
                }

                const connectButton = document.querySelector('button[aria-label="Проиграть звук"]');
                if (connectButton) {
                    connectButton.click();
                    clearInterval(this.intervals.bbbButtonCheck);
                    this.intervals.bbbButtonCheck = null;
                }
            }, 2000);

            setTimeout(() => {
                if (this.intervals.bbbButtonCheck) {
                    clearInterval(this.intervals.bbbButtonCheck);
                    this.intervals.bbbButtonCheck = null;
                }
            }, 60000);
        },

        startAutoHello() {
            if (this.intervals.hello) return;
            this.flags.helloMessageSent = false;

            this.intervals.hello = setInterval(() => {
                if (this.flags.helloMessageSent) {
                    this.stopAutoHello();
                    return;
                }

                const greetings = ["здравствуйте", "здравстуйте", "добрый день", "доброе утро"];
                const pageText = document.body.innerText.toLowerCase();

                if (greetings.some(greet => pageText.includes(greet))) {
                    const messageInput = document.querySelector('#message-input');
                    const sendButton = document.querySelector('button[aria-label="Отправить сообщение"]');

                    if (messageInput && sendButton) {
                        const message = "Здравствуйте";

                        let reactProps = null;
                        try { reactProps = this.findReactProps(messageInput); } catch (e) {}

                        if (reactProps && reactProps.onChange) {
                            const syntheticEvent = { target: { value: message }, currentTarget: { value: message } };
                            reactProps.onChange(syntheticEvent);
                            sendButton.click();
                            this.flags.helloMessageSent = true;
                        } else {
                            messageInput.value = message;
                            messageInput.dispatchEvent(new Event('input', { bubbles: true }));
                            setTimeout(() => {
                                sendButton.click();
                                this.flags.helloMessageSent = true;
                            }, 100);
                        }
                    }
                }
            }, 2000);
        },
        stopAutoHello() {
            clearInterval(this.intervals.hello);
            this.intervals.hello = null;
        },
        findReactProps(dom) {
            for (const key in dom) {
                if (key.startsWith('__reactInternalInstance$') || key.startsWith('__reactFiber$')) {
                    let fiber = dom[key];
                    if (fiber.return) {
                        let current = fiber.return;
                        while(current) {
                            if (current.stateNode && current.stateNode.props) return current.stateNode.props;
                            current = current.return;
                        }
                    }
                    if (fiber._currentElement && fiber._currentElement._owner && fiber._currentElement._owner._instance) {
                        return fiber._currentElement._owner._instance.props;
                    }
                }
            }
            return null;
        },

        startAutoLeave() {
            if (this.intervals.leave) return;

            this.intervals.leave = setInterval(() => {
                this.checkLeaveText();
            }, 5000);
        },
        stopAutoLeave() {
            clearInterval(this.intervals.leave);
            this.intervals.leave = null;
        },
        disablePopups() {
            var beforeScript = document.createElement('script');
            beforeScript.textContent = `
        Window.prototype.addEventListener2 = Window.prototype.addEventListener;
        Window.prototype.addEventListener = function(type, listener, useCapture) {
            if (type != "beforeunload") {
                addEventListener2(type, listener, useCapture);
            }
        }
    `;
            (document.head||document.documentElement).insertBefore(beforeScript, (document.head||document.documentElement).firstChild);
            beforeScript.onload = function() {
                this.parentNode.removeChild(this);
            };

            var afterScript = document.createElement('script');
            afterScript.textContent = `
        function letmeout() {
            var all = document.getElementsByTagName("*");
            for (var i=0, max=all.length; i < max; i++) {
                if(all[i].getAttribute("onbeforeunload")) {
                    all[i].setAttribute("onbeforeunload", null);
                }
            }
            window.onbeforeunload = null;
        }
        letmeout();
        setInterval(letmeout, 500);
    `;
            (document.head||document.documentElement).appendChild(afterScript);
            afterScript.onload = function() {
                this.parentNode.removeChild(this);
            };
        },
        checkLeaveText() {
            var text = document.body.innerText.toLowerCase();
            if (text.includes('до свидания') || text.includes('досвидания')) {
                this.disablePopups();
                window.close();
            }
        },

        run(isConnectPage, isConferencePage) {
            this.initUI(isConnectPage, isConferencePage);

            if (isConnectPage && this.settings.autoConnect) {
                this.startAutoConnect();
            }
            if (isConferencePage) {
                this.handleHtml5ClientPage();
                if (this.settings.autoHello) this.startAutoHello();
                if (this.settings.autoLeave) this.startAutoLeave();
            }
        }
    };

    const AutoBRS = {
        settings: {
            enabled: GM_getValue(SETTINGS_KEYS.autoBRS, false)
        },
        intervals: {
            check: null
        },
        timeouts: {
            reload: null
        },
        buttonId: 'modalCurrentLessonForMarkButtonOK',
        toggleButtonId: 'toggleBRS',

        init() {
            this.insertToggleButton();
            if (this.settings.enabled) {
                this.start();
            }
        },
        insertToggleButton() {
            const navbar = document.querySelector('ul.navbar-nav.nav-tabs');
            if (!navbar || document.getElementById(this.toggleButtonId)) return;

            const navItem = document.createElement('li');
            navItem.className = 'nav-item moodle-autotool-nav-item';

            const toggleBtn = createToggleButton(
                this.toggleButtonId,
                'AutoBRS',
                SETTINGS_KEYS.autoBRS,
                this.settings.enabled,
                (enabled) => {
                    this.settings.enabled = enabled;
                    enabled ? this.start() : this.stop();
                }
            );
            toggleBtn.classList.add('nav-link');

            navItem.appendChild(toggleBtn);
            navbar.appendChild(navItem);
        },
        checkAndClick() {
            const button = document.getElementById(this.buttonId);
            if (button) {
                button.click();
            }
        },
        start() {
            if (this.intervals.check) return;
            this.settings.enabled = true;
            const toggleBtn = document.getElementById(this.toggleButtonId);
            if (toggleBtn && !toggleBtn.classList.contains('on')) {
                toggleBtn.classList.remove('off');
                toggleBtn.classList.add('on');
                toggleBtn.textContent = 'AutoBRS: ВКЛ';
            }

            this.intervals.check = setInterval(() => this.checkAndClick(), 1000);

            this.timeouts.reload = setTimeout(() => {
                if (this.settings.enabled && !document.getElementById(this.buttonId)) {
                    location.reload();
                }
            }, 10000);
        },
        stop() {
            clearInterval(this.intervals.check);
            clearTimeout(this.timeouts.reload);
            this.intervals.check = null;
            this.timeouts.reload = null;
            this.settings.enabled = false;
            const toggleBtn = document.getElementById(this.toggleButtonId);
            if (toggleBtn && !toggleBtn.classList.contains('off')) {
                toggleBtn.classList.remove('on');
                toggleBtn.classList.add('off');
                toggleBtn.textContent = 'AutoBRS: ВЫКЛ';
            }
        }
    };


    const AutoFAC = {
        interval: null,
        buttonSelector: '[aria-label="Проверка"]',

        init() {
            this.start();
        },
        autoClick() {
            const facButton = document.querySelector(this.buttonSelector);
            if (facButton) {
                facButton.click();
            }
        },
        start() {
            if (this.interval) return;
            this.interval = setInterval(() => this.autoClick(), 5000);
        },
        stop() {
            if (this.interval) {
                clearInterval(this.interval);
                this.interval = null;
            }
        }
    };

    const AutoAttendance = {
        settings: {
            enabled: GM_getValue(SETTINGS_KEYS.autoAttendance, false)
        },
        intervals: {
            check: null
        },
        timeouts: {
            reload: null
        },
        toggleButtonId: 'toggleAttendance',

        init() {
            this.insertToggleButton();
            if (this.settings.enabled) {
                this.start();
            }
        },
        insertToggleButton() {
            const navBar = document.querySelector('ul.nav.nav-tabs');
            if (!navBar || document.getElementById(this.toggleButtonId)) return;

            const navItem = document.createElement('li');
            navItem.className = 'nav-item moodle-autotool-nav-item';

            const toggleBtn = createToggleButton(
                this.toggleButtonId,
                'AutoAttendance',
                SETTINGS_KEYS.autoAttendance,
                this.settings.enabled,
                (enabled) => {
                    this.settings.enabled = enabled;
                    enabled ? this.start() : this.stop();
                }
            );
            toggleBtn.classList.add('nav-link');

            navItem.appendChild(toggleBtn);
            navBar.appendChild(navItem);
        },
        processPage() {
            const submitButton = document.querySelector('input[type="submit"][value="Сохранить"].btn.btn-primary');

            if (submitButton) {
                const radioInput = document.querySelector('input[type="radio"].form-check-input[name="status"]');
                if (radioInput) {
                    radioInput.click();
                    submitButton.click();
                    this.stop();
                    return true;
                }
            } else {
                const attendanceTd = Array.from(document.querySelectorAll('td')).find(td => td.textContent.includes("Отметить свое присутствие"));

                if (attendanceTd) {
                    const attendanceLink = attendanceTd.querySelector('a');
                    if (attendanceLink) {
                        window.location.href = attendanceLink.href;
                        this.stop();
                        return true;
                    }
                } else {
                    if (this.settings.enabled && !this.timeouts.reload) {
                        this.timeouts.reload = setTimeout(() => {
                            location.reload();
                        }, 10000);
                    }
                }
            }
            return false;
        },
        start() {
            if (this.intervals.check) return;
            this.settings.enabled = true;
            const toggleBtn = document.getElementById(this.toggleButtonId);
            if (toggleBtn && !toggleBtn.classList.contains('on')) {
                toggleBtn.classList.remove('off');
                toggleBtn.classList.add('on');
                toggleBtn.textContent = 'AutoAttendance: ВКЛ';
            }

            if (this.processPage()) return;

            this.intervals.check = setInterval(() => {
                this.processPage();
            }, 1000);
        },
        stop() {
            clearInterval(this.intervals.check);
            clearTimeout(this.timeouts.reload);
            this.intervals.check = null;
            this.timeouts.reload = null;
            this.settings.enabled = false;
            const toggleBtn = document.getElementById(this.toggleButtonId);
            if (toggleBtn && !toggleBtn.classList.contains('off')) {
                toggleBtn.classList.remove('on');
                toggleBtn.classList.add('off');
                toggleBtn.textContent = 'AutoAttendance: ВЫКЛ';
            }
        }
    };

    function run() {
        const href = window.location.href;

        if (href.includes('/mod/bigbluebuttonbn/view.php')) {
            AutoTools.run(true, false);
        } else if (href.includes('/html5client/')) {
            AutoTools.run(false, true);
            AutoFAC.init();
        }
        else if (href.includes('/brs/att_marks_report_student/')) {
            AutoBRS.init();
        }
        else if (href.includes('/mod/attendance/')) {
            AutoAttendance.init();
        }
    }

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

})();