Moodle AutoPilot

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

  1. // ==UserScript==
  2. // @name Moodle AutoPilot
  3. // @namespace https://t.me/johannmosin
  4. // @version 1.0.5
  5. // @description Полный набор для 100% посещаемости дистанционных лекций
  6. // @author Johann Mosin
  7. // @match https://edu.vsu.ru/mod/bigbluebuttonbn/view.php*
  8. // @match https://*.edu.vsu.ru/html5client/*
  9. // @match https://www.cs.vsu.ru/brs/att_marks_report_student/*
  10. // @match https://edu.vsu.ru/mod/attendance/*
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_addStyle
  14. // @license MIT
  15. // @icon https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Glagolitic_ljudi.svg/47px-Glagolitic_ljudi.svg.png
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // --- Settings Keys ---
  22. const SETTINGS_KEYS = {
  23. autoConnect: 'autoConnectEnabled',
  24. autoHello: 'autoHelloEnabled',
  25. autoLeave: 'autoLeaveEnabled',
  26. autoBRS: 'autobrsEnabled',
  27. autoAttendance: 'autoAttendanceEnabled'
  28. };
  29.  
  30. // --- Styles ---
  31. GM_addStyle(`
  32. .moodle-autotool-button {
  33. cursor: pointer;
  34. border-radius: 7px;
  35. padding: 8px 15px !important;
  36. transition: background 0.3s;
  37. color: black !important;
  38. border: none !important;
  39. margin: 5px;
  40. text-align: center;
  41. font-size: 1rem;
  42. line-height: 1.5;
  43. }
  44. .moodle-autotool-button:hover {
  45. opacity: 0.9;
  46. }
  47. .moodle-autotool-button.off {
  48. background: rgba(255, 193, 7, 0.25);
  49. }
  50. .moodle-autotool-button.on {
  51. background: rgba(0, 128, 0, 0.25) !important;
  52. }
  53.  
  54. .autoTool-controls {
  55. display: flex;
  56. margin: 10px 0;
  57. flex-wrap: wrap;
  58. }
  59. .autoTool-button {
  60. width: 180px;
  61. }
  62.  
  63. #toggleBRS {
  64. width: 150px;
  65. margin-bottom: 2px;
  66. }
  67.  
  68. #toggleAttendance {
  69. margin-top: 2px;
  70. margin-bottom: 2px;
  71. }
  72.  
  73. .moodle-autotool-nav-item {
  74. display: flex;
  75. align-items: center;
  76. }
  77. `);
  78.  
  79. function createToggleButton(id, textPrefix, settingKey, initialState = false, onClickCallback = null) {
  80. const button = document.createElement('button');
  81. button.id = id;
  82. button.className = `moodle-autotool-button ${id === 'autoConnectBtn' || id === 'autoHelloBtn' || id === 'autoLeaveBtn' ? 'autoTool-button' : ''}`;
  83. button.dataset.settingKey = settingKey;
  84.  
  85. const updateButtonState = (btn, enabled) => {
  86. btn.textContent = `${textPrefix}: ${enabled ? 'ВКЛ' : 'ВЫКЛ'}`;
  87. if (enabled) {
  88. btn.classList.remove('off');
  89. btn.classList.add('on');
  90. } else {
  91. btn.classList.remove('on');
  92. btn.classList.add('off');
  93. }
  94. };
  95.  
  96. let isEnabled = GM_getValue(settingKey, initialState);
  97. updateButtonState(button, isEnabled);
  98.  
  99. button.addEventListener('click', (e) => {
  100. e.preventDefault();
  101. isEnabled = !isEnabled;
  102. GM_setValue(settingKey, isEnabled);
  103. updateButtonState(button, isEnabled);
  104. if (onClickCallback) {
  105. onClickCallback(isEnabled);
  106. }
  107. });
  108.  
  109. return button;
  110. }
  111.  
  112. const AutoTools = {
  113. settings: {
  114. autoConnect: GM_getValue(SETTINGS_KEYS.autoConnect, false),
  115. autoHello: GM_getValue(SETTINGS_KEYS.autoHello, false),
  116. autoLeave: GM_getValue(SETTINGS_KEYS.autoLeave, false)
  117. },
  118. intervals: {
  119. connect: null,
  120. hello: null,
  121. leave: null,
  122. bbbButtonCheck: null
  123. },
  124. timeouts: {
  125. reload: null
  126. },
  127. flags: {
  128. connectCheckStarted: false,
  129. helloMessageSent: false
  130. },
  131.  
  132. initUI(isConnectPage, isConferencePage) {
  133. if (document.querySelector('.autoTool-controls')) return;
  134.  
  135. const controlPanel = document.createElement('div');
  136. controlPanel.className = 'autoTool-controls';
  137.  
  138. const createAndAppend = (id, text, key, callback) => {
  139. const btn = createToggleButton(id, text, key, this.settings[key.replace('Enabled','')], callback);
  140. controlPanel.appendChild(btn);
  141. };
  142.  
  143. if (isConnectPage) {
  144. createAndAppend('autoConnectBtn', 'AutoConnect', SETTINGS_KEYS.autoConnect, (enabled) => {
  145. this.settings.autoConnect = enabled;
  146. enabled ? this.startAutoConnect() : this.stopAutoConnect();
  147. });
  148. }
  149.  
  150. if (isConnectPage || isConferencePage) {
  151. createAndAppend('autoHelloBtn', 'AutoHello', SETTINGS_KEYS.autoHello, (enabled) => {
  152. this.settings.autoHello = enabled;
  153. enabled ? this.startAutoHello() : this.stopAutoHello();
  154. });
  155. createAndAppend('autoLeaveBtn', 'AutoLeave', SETTINGS_KEYS.autoLeave, (enabled) => {
  156. this.settings.autoLeave = enabled;
  157. enabled ? this.startAutoLeave() : this.stopAutoLeave();
  158. });
  159. }
  160.  
  161. if (controlPanel.hasChildNodes()) {
  162. if (isConnectPage) {
  163. const targetElement = document.querySelector('[class*="custom-select"]') || document.querySelector('#region-main') || document.body;
  164. if(targetElement === document.body) targetElement.insertBefore(controlPanel, targetElement.firstChild);
  165. else targetElement.parentNode.insertBefore(controlPanel, targetElement.nextSibling);
  166. } else if (isConferencePage) {
  167. const userListContent = document.querySelector('[data-test="userList"]');
  168. const chatInputArea = document.querySelector('#message-input')?.parentNode;
  169. const targetParent = userListContent?.parentNode || chatInputArea || document.body;
  170. const referenceNode = userListContent || (chatInputArea ? chatInputArea.firstChild : null) || document.body.firstChild;
  171.  
  172. const observer = new MutationObserver((mutations, obs) => {
  173. let inserted = false;
  174. const userList = document.querySelector('[data-test="userList"]');
  175. const chatInput = document.querySelector('#message-input')?.parentNode;
  176. if (userList) {
  177. userList.parentNode.insertBefore(controlPanel, userList);
  178. inserted = true;
  179. } else if (chatInput) {
  180. chatInput.parentNode.insertBefore(controlPanel, chatInput);
  181. inserted = true;
  182. }
  183. if (inserted) {
  184. obs.disconnect();
  185. }
  186. });
  187.  
  188. if (userListContent) {
  189. userListContent.parentNode.insertBefore(controlPanel, userListContent);
  190. } else if (chatInputArea) {
  191. chatInputArea.parentNode.insertBefore(controlPanel, chatInputArea);
  192. } else {
  193. observer.observe(document.body, { childList: true, subtree: true });
  194. setTimeout(() => {
  195. observer.disconnect();
  196. if (!document.querySelector('.autoTool-controls')) {
  197. document.body.insertBefore(controlPanel, document.body.firstChild);
  198. }
  199. }, 15000);
  200. }
  201. }
  202. }
  203. },
  204.  
  205. startAutoConnect() {
  206. if (!this.flags.connectCheckStarted) {
  207. this.flags.connectCheckStarted = true;
  208. this.timeouts.reload = setTimeout(() => {
  209. this.checkForSessionLink();
  210. }, 10000);
  211. }
  212. },
  213. stopAutoConnect() {
  214. clearInterval(this.intervals.connect);
  215. clearTimeout(this.timeouts.reload);
  216. this.intervals.connect = null;
  217. this.timeouts.reload = null;
  218. this.flags.connectCheckStarted = false;
  219. },
  220. resetReloadTimeout() {
  221. clearTimeout(this.timeouts.reload);
  222. this.timeouts.reload = setTimeout(() => {
  223. if (this.settings.autoConnect) {
  224. location.reload();
  225. }
  226. }, 10000);
  227. },
  228. checkForSessionLink() {
  229. const sessionLink = Array.from(document.querySelectorAll('a')).find(a =>
  230. a.textContent.includes("Подключиться к сеансу"));
  231.  
  232. if (sessionLink && sessionLink.href) {
  233. window.open(sessionLink.href, '_blank');
  234. this.stopAutoConnect();
  235. } else {
  236. this.resetReloadTimeout();
  237. }
  238. },
  239.  
  240. handleHtml5ClientPage() {
  241. this.intervals.bbbButtonCheck = setInterval(() => {
  242. const joinButton = document.querySelector('button[aria-label="Только слушать"]');
  243. if (joinButton) {
  244. joinButton.click();
  245. }
  246.  
  247. const connectButton = document.querySelector('button[aria-label="Проиграть звук"]');
  248. if (connectButton) {
  249. connectButton.click();
  250. clearInterval(this.intervals.bbbButtonCheck);
  251. this.intervals.bbbButtonCheck = null;
  252. }
  253. }, 2000);
  254.  
  255. setTimeout(() => {
  256. if (this.intervals.bbbButtonCheck) {
  257. clearInterval(this.intervals.bbbButtonCheck);
  258. this.intervals.bbbButtonCheck = null;
  259. }
  260. }, 60000);
  261. },
  262.  
  263. startAutoHello() {
  264. if (this.intervals.hello) return;
  265. this.flags.helloMessageSent = false;
  266.  
  267. this.intervals.hello = setInterval(() => {
  268. if (this.flags.helloMessageSent) {
  269. this.stopAutoHello();
  270. return;
  271. }
  272.  
  273. const greetings = ["здравствуйте", "здравстуйте", "добрый день", "доброе утро"];
  274. const pageText = document.body.innerText.toLowerCase();
  275.  
  276. if (greetings.some(greet => pageText.includes(greet))) {
  277. const messageInput = document.querySelector('#message-input');
  278. const sendButton = document.querySelector('button[aria-label="Отправить сообщение"]');
  279.  
  280. if (messageInput && sendButton) {
  281. const message = "Здравствуйте";
  282.  
  283. let reactProps = null;
  284. try { reactProps = this.findReactProps(messageInput); } catch (e) {}
  285.  
  286. if (reactProps && reactProps.onChange) {
  287. const syntheticEvent = { target: { value: message }, currentTarget: { value: message } };
  288. reactProps.onChange(syntheticEvent);
  289. sendButton.click();
  290. this.flags.helloMessageSent = true;
  291. } else {
  292. messageInput.value = message;
  293. messageInput.dispatchEvent(new Event('input', { bubbles: true }));
  294. setTimeout(() => {
  295. sendButton.click();
  296. this.flags.helloMessageSent = true;
  297. }, 100);
  298. }
  299. }
  300. }
  301. }, 2000);
  302. },
  303. stopAutoHello() {
  304. clearInterval(this.intervals.hello);
  305. this.intervals.hello = null;
  306. },
  307. findReactProps(dom) {
  308. for (const key in dom) {
  309. if (key.startsWith('__reactInternalInstance$') || key.startsWith('__reactFiber$')) {
  310. let fiber = dom[key];
  311. if (fiber.return) {
  312. let current = fiber.return;
  313. while(current) {
  314. if (current.stateNode && current.stateNode.props) return current.stateNode.props;
  315. current = current.return;
  316. }
  317. }
  318. if (fiber._currentElement && fiber._currentElement._owner && fiber._currentElement._owner._instance) {
  319. return fiber._currentElement._owner._instance.props;
  320. }
  321. }
  322. }
  323. return null;
  324. },
  325.  
  326. startAutoLeave() {
  327. if (this.intervals.leave) return;
  328.  
  329. this.intervals.leave = setInterval(() => {
  330. this.checkLeaveText();
  331. }, 5000);
  332. },
  333. stopAutoLeave() {
  334. clearInterval(this.intervals.leave);
  335. this.intervals.leave = null;
  336. },
  337. disablePopups() {
  338. var beforeScript = document.createElement('script');
  339. beforeScript.textContent = `
  340. Window.prototype.addEventListener2 = Window.prototype.addEventListener;
  341. Window.prototype.addEventListener = function(type, listener, useCapture) {
  342. if (type != "beforeunload") {
  343. addEventListener2(type, listener, useCapture);
  344. }
  345. }
  346. `;
  347. (document.head||document.documentElement).insertBefore(beforeScript, (document.head||document.documentElement).firstChild);
  348. beforeScript.onload = function() {
  349. this.parentNode.removeChild(this);
  350. };
  351.  
  352. var afterScript = document.createElement('script');
  353. afterScript.textContent = `
  354. function letmeout() {
  355. var all = document.getElementsByTagName("*");
  356. for (var i=0, max=all.length; i < max; i++) {
  357. if(all[i].getAttribute("onbeforeunload")) {
  358. all[i].setAttribute("onbeforeunload", null);
  359. }
  360. }
  361. window.onbeforeunload = null;
  362. }
  363. letmeout();
  364. setInterval(letmeout, 500);
  365. `;
  366. (document.head||document.documentElement).appendChild(afterScript);
  367. afterScript.onload = function() {
  368. this.parentNode.removeChild(this);
  369. };
  370. },
  371. checkLeaveText() {
  372. var text = document.body.innerText.toLowerCase();
  373. if (text.includes('до свидания') || text.includes('досвидания')) {
  374. this.disablePopups();
  375. window.close();
  376. }
  377. },
  378.  
  379. run(isConnectPage, isConferencePage) {
  380. this.initUI(isConnectPage, isConferencePage);
  381.  
  382. if (isConnectPage && this.settings.autoConnect) {
  383. this.startAutoConnect();
  384. }
  385. if (isConferencePage) {
  386. this.handleHtml5ClientPage();
  387. if (this.settings.autoHello) this.startAutoHello();
  388. if (this.settings.autoLeave) this.startAutoLeave();
  389. }
  390. }
  391. };
  392.  
  393. const AutoBRS = {
  394. settings: {
  395. enabled: GM_getValue(SETTINGS_KEYS.autoBRS, false)
  396. },
  397. intervals: {
  398. check: null
  399. },
  400. timeouts: {
  401. reload: null
  402. },
  403. buttonId: 'modalCurrentLessonForMarkButtonOK',
  404. toggleButtonId: 'toggleBRS',
  405.  
  406. init() {
  407. this.insertToggleButton();
  408. if (this.settings.enabled) {
  409. this.start();
  410. }
  411. },
  412. insertToggleButton() {
  413. const navbar = document.querySelector('ul.navbar-nav.nav-tabs');
  414. if (!navbar || document.getElementById(this.toggleButtonId)) return;
  415.  
  416. const navItem = document.createElement('li');
  417. navItem.className = 'nav-item moodle-autotool-nav-item';
  418.  
  419. const toggleBtn = createToggleButton(
  420. this.toggleButtonId,
  421. 'AutoBRS',
  422. SETTINGS_KEYS.autoBRS,
  423. this.settings.enabled,
  424. (enabled) => {
  425. this.settings.enabled = enabled;
  426. enabled ? this.start() : this.stop();
  427. }
  428. );
  429. toggleBtn.classList.add('nav-link');
  430.  
  431. navItem.appendChild(toggleBtn);
  432. navbar.appendChild(navItem);
  433. },
  434. checkAndClick() {
  435. const button = document.getElementById(this.buttonId);
  436. if (button) {
  437. button.click();
  438. }
  439. },
  440. start() {
  441. if (this.intervals.check) return;
  442. this.settings.enabled = true;
  443. const toggleBtn = document.getElementById(this.toggleButtonId);
  444. if (toggleBtn && !toggleBtn.classList.contains('on')) {
  445. toggleBtn.classList.remove('off');
  446. toggleBtn.classList.add('on');
  447. toggleBtn.textContent = 'AutoBRS: ВКЛ';
  448. }
  449.  
  450. this.intervals.check = setInterval(() => this.checkAndClick(), 1000);
  451.  
  452. this.timeouts.reload = setTimeout(() => {
  453. if (this.settings.enabled && !document.getElementById(this.buttonId)) {
  454. location.reload();
  455. }
  456. }, 10000);
  457. },
  458. stop() {
  459. clearInterval(this.intervals.check);
  460. clearTimeout(this.timeouts.reload);
  461. this.intervals.check = null;
  462. this.timeouts.reload = null;
  463. this.settings.enabled = false;
  464. const toggleBtn = document.getElementById(this.toggleButtonId);
  465. if (toggleBtn && !toggleBtn.classList.contains('off')) {
  466. toggleBtn.classList.remove('on');
  467. toggleBtn.classList.add('off');
  468. toggleBtn.textContent = 'AutoBRS: ВЫКЛ';
  469. }
  470. }
  471. };
  472.  
  473.  
  474. const AutoFAC = {
  475. interval: null,
  476. buttonSelector: '[aria-label="Проверка"]',
  477.  
  478. init() {
  479. this.start();
  480. },
  481. autoClick() {
  482. const facButton = document.querySelector(this.buttonSelector);
  483. if (facButton) {
  484. facButton.click();
  485. }
  486. },
  487. start() {
  488. if (this.interval) return;
  489. this.interval = setInterval(() => this.autoClick(), 5000);
  490. },
  491. stop() {
  492. if (this.interval) {
  493. clearInterval(this.interval);
  494. this.interval = null;
  495. }
  496. }
  497. };
  498.  
  499. const AutoAttendance = {
  500. settings: {
  501. enabled: GM_getValue(SETTINGS_KEYS.autoAttendance, false)
  502. },
  503. intervals: {
  504. check: null
  505. },
  506. timeouts: {
  507. reload: null
  508. },
  509. toggleButtonId: 'toggleAttendance',
  510.  
  511. init() {
  512. this.insertToggleButton();
  513. if (this.settings.enabled) {
  514. this.start();
  515. }
  516. },
  517. insertToggleButton() {
  518. const navBar = document.querySelector('ul.nav.nav-tabs');
  519. if (!navBar || document.getElementById(this.toggleButtonId)) return;
  520.  
  521. const navItem = document.createElement('li');
  522. navItem.className = 'nav-item moodle-autotool-nav-item';
  523.  
  524. const toggleBtn = createToggleButton(
  525. this.toggleButtonId,
  526. 'AutoAttendance',
  527. SETTINGS_KEYS.autoAttendance,
  528. this.settings.enabled,
  529. (enabled) => {
  530. this.settings.enabled = enabled;
  531. enabled ? this.start() : this.stop();
  532. }
  533. );
  534. toggleBtn.classList.add('nav-link');
  535.  
  536. navItem.appendChild(toggleBtn);
  537. navBar.appendChild(navItem);
  538. },
  539. processPage() {
  540. const submitButton = document.querySelector('input[type="submit"][value="Сохранить"].btn.btn-primary');
  541.  
  542. if (submitButton) {
  543. const radioInput = document.querySelector('input[type="radio"].form-check-input[name="status"]');
  544. if (radioInput) {
  545. radioInput.click();
  546. submitButton.click();
  547. this.stop();
  548. return true;
  549. }
  550. } else {
  551. const attendanceTd = Array.from(document.querySelectorAll('td')).find(td => td.textContent.includes("Отметить свое присутствие"));
  552.  
  553. if (attendanceTd) {
  554. const attendanceLink = attendanceTd.querySelector('a');
  555. if (attendanceLink) {
  556. window.location.href = attendanceLink.href;
  557. this.stop();
  558. return true;
  559. }
  560. } else {
  561. if (this.settings.enabled && !this.timeouts.reload) {
  562. this.timeouts.reload = setTimeout(() => {
  563. location.reload();
  564. }, 10000);
  565. }
  566. }
  567. }
  568. return false;
  569. },
  570. start() {
  571. if (this.intervals.check) return;
  572. this.settings.enabled = true;
  573. const toggleBtn = document.getElementById(this.toggleButtonId);
  574. if (toggleBtn && !toggleBtn.classList.contains('on')) {
  575. toggleBtn.classList.remove('off');
  576. toggleBtn.classList.add('on');
  577. toggleBtn.textContent = 'AutoAttendance: ВКЛ';
  578. }
  579.  
  580. if (this.processPage()) return;
  581.  
  582. this.intervals.check = setInterval(() => {
  583. this.processPage();
  584. }, 1000);
  585. },
  586. stop() {
  587. clearInterval(this.intervals.check);
  588. clearTimeout(this.timeouts.reload);
  589. this.intervals.check = null;
  590. this.timeouts.reload = null;
  591. this.settings.enabled = false;
  592. const toggleBtn = document.getElementById(this.toggleButtonId);
  593. if (toggleBtn && !toggleBtn.classList.contains('off')) {
  594. toggleBtn.classList.remove('on');
  595. toggleBtn.classList.add('off');
  596. toggleBtn.textContent = 'AutoAttendance: ВЫКЛ';
  597. }
  598. }
  599. };
  600.  
  601. function run() {
  602. const href = window.location.href;
  603.  
  604. if (href.includes('/mod/bigbluebuttonbn/view.php')) {
  605. AutoTools.run(true, false);
  606. } else if (href.includes('/html5client/')) {
  607. AutoTools.run(false, true);
  608. AutoFAC.init();
  609. }
  610. else if (href.includes('/brs/att_marks_report_student/')) {
  611. AutoBRS.init();
  612. }
  613. else if (href.includes('/mod/attendance/')) {
  614. AutoAttendance.init();
  615. }
  616. }
  617.  
  618. if (document.readyState === 'loading') {
  619. document.addEventListener('DOMContentLoaded', run);
  620. } else {
  621. run();
  622. }
  623.  
  624. })();