Moodle AutoPilot

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();