// ==UserScript==
// @name Moodle AutoPilot
// @namespace https://t.me/johannmosin
// @version 1.0.3
// @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.disablePopups();
this.intervals.leave = setInterval(() => {
this.disablePopups();
this.checkLeaveText();
}, 5000);
},
stopAutoLeave() {
clearInterval(this.intervals.leave);
this.intervals.leave = null;
},
disablePopups() {
if (window.onbeforeunload !== null) {
window.onbeforeunload = null;
}
},
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();
}
})();