// ==UserScript== // @name UIT - Auto Lecture Survey (UALS) // @version 3.0.1 // @author Kevin Nitro // @namespace https://github.com/KevinNitroG // @description Userscript tự động khảo sát môn học UIT. Khuyến nghị disable script khi không sử dụng, tránh conflict với các khảo sát / link khác của trường. // @downloadURL https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey/raw/main/src/uals.user.js // @updateURL https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey/raw/main/src/uals.user.js // @supportURL https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey/issues // @license https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey/raw/main/LICENSE // @icon https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey/raw/main/assets/images/UIT-logo.png // @run-at document-idle // @match http*://student.uit.edu.vn/sinhvien/phieukhaosat // @match http*://survey.uit.edu.vn/index.php/survey/index // @match http*://survey.uit.edu.vn/index.php/survey/index/sid/*/token/* // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_getValue // @grant GM_listValues // @grant GM_notification // @grant GM_openInTab // @grant GM_setValue // @grant window.close // ==/UserScript== (function () { 'use strict'; const SELECTIONS = { first: { question: 'Tỷ lệ thời gian Anh/Chị lên lớp đối với môn học này', rawQuestions: [ '*Tỷ lệ thời gian Anh/Chị lên lớp đối với môn học này\n', '* Tỷ lệ thời gian Anh/Chị lên lớp đối với môn học này\n', ], answers: [ { label: '<50%', selector: 'ul:nth-child(1) input' }, { label: '50-80%', selector: 'ul:nth-child(2) input' }, { label: '>80%', selector: 'ul:nth-child(3) input' }, ], }, second: { question: 'Anh chị tự đánh giá đạt được bao nhiêu % chuẩn đầu ra của môn học này', rawQuestions: [ '*Anh chị tự đánh giá đạt được bao nhiêu % chuẩn đầu ra của môn học này:\n', ], answers: [ { label: 'Không biết chuẩn đầu ra là gì', selector: 'ul:nth-child(1) li:nth-child(1) input', }, { label: 'Dưới 50%', selector: 'ul:nth-child(1) li:nth-child(2) input', }, { label: 'Từ 50 đến dưới 70%', selector: 'ul:nth-child(1) li:nth-child(3) input', }, { label: 'Từ 70 đến dưới 90%', selector: 'ul:nth-child(2) li:nth-child(1) input', }, { label: 'Trên 90%', selector: 'ul:nth-child(2) li:nth-child(2) input', }, ], }, third: { question: 'Đánh giá về hoạt động giảng dạy trực tuyến của Giảng viên', container: 'table[summary="Đánh giá về hoạt động giảng dạy trực tuyến của Giảng viên - an array type question"] .answers-list.radio-list', answers: [ { selector: '.answer_cell_00MH01.answer-item.radio-item' }, { selector: '.answer_cell_00MH02.answer-item.radio-item' }, { selector: '.answer_cell_00MH03.answer-item.radio-item' }, { selector: '.answer_cell_00MH04.answer-item.radio-item' }, ], }, }; const CONTINUE_BUTTON_SELECTOR = 'button[type="submit"][id="movenextbtn"]'; const SUBMIT_BUTTON_SELECT = '#movesubmitbtn'; const GM_BROADCAST_KEY_NAME = 'broadcast'; const STYLE = ` #uals__container { align-items: center; display: flex; flex-direction: column; justify-content: center; } .uals__btn-container { align-items: center; display: flex; justify-content: center; } .uals__btn { background-color: #115d9d; border-radius: 0.5rem; border: none; color: white; margin: 0.4rem 0.3rem; padding: 0.4rem 0.5rem; transition: background-color 0.3s ease-in-out; } .uals__btn:hover { background-color: #1678cb; } #uals__menu-container { display: none; overflow: hidden; transition: height 0.5s; } .uals__menu-container--show { display: inline-block !important; height: auto; } .uals__run-btn--unavailable, .uals__run-btn--unavailable:hover { cursor: default; filter: brightness(70%); } .uals__run-btn--running { background-color: red !important; } .uals__please_star { align-items: center; display: flex; justify-content: center; } .uals__form-select { align-items: center; display: flex; flex-direction: row; } .uals__form-select>label { margin: 0 0.5rem 0 0; padding: 0 0.5rem 0 0.5rem; vertical-align: middle; } `; // NOTE: Some of methods can be marked as static but I'm too tired to try and check if it works or not function getRandomElement(array) { const randomIndex = Math.floor(Math.random() * array.length); return array[randomIndex]; } class Model { #firstOpts; #secondOpts; #thirdOpts; static #firstOptsKey = 'userFirstOpts'; static #secondOptsKey = 'usersecondOpts'; static #thirdOptsKey = 'userthirdOpts'; constructor() { this.#firstOpts = GM_getValue(Model.#firstOptsKey, []); this.#secondOpts = GM_getValue(Model.#secondOptsKey, []); this.#thirdOpts = GM_getValue(Model.#thirdOptsKey, []); } addStyles() { GM_addStyle(STYLE); } setUserOpts(userOpts) { this.#firstOpts = userOpts.firstOpts; this.#secondOpts = userOpts.secondOpts; this.#thirdOpts = userOpts.thirdOpts; } getUserOpts() { return { firstOpts: this.#firstOpts, secondOpts: this.#secondOpts, thirdOpts: this.#thirdOpts, }; } saveUserOpts() { GM_setValue(Model.#firstOptsKey, this.#firstOpts); GM_setValue(Model.#secondOptsKey, this.#secondOpts); GM_setValue(Model.#thirdOptsKey, this.#thirdOpts); } static deleteUserOpts() { const keys = GM_listValues(); for (const key of keys) { GM_deleteValue(key); } } checkUserOptsExist() { if ( this.#firstOpts.length === 0 || this.#secondOpts.length === 0 || this.#thirdOpts.length === 0 ) { GM_notification({ text: 'Bạn cần thiết lập các tuỳ chọn 🥵', title: 'UALS', tag: 'uals-require_config', timeout: 3000, }); return false; } return true; } } class BroadcastSvc { #id; constructor(callback) { this.#id = GM_addValueChangeListener(GM_BROADCAST_KEY_NAME, callback); } removeReceiveMsgListener() { GM_deleteValue(GM_BROADCAST_KEY_NAME); GM_removeValueChangeListener(this.#id); } static sendDone() { GM_setValue(GM_BROADCAST_KEY_NAME, BroadcastSvc._genRandomVal()); } static _genRandomVal() { const min = Number.MIN_SAFE_INTEGER; // -9007199254740991 const max = Number.MAX_SAFE_INTEGER; // 9007199254740991 let val; do { val = Math.floor(Math.random() * (max - min + 1)) + min; } while (val === GM_getValue(GM_BROADCAST_KEY_NAME, null)); return val; } } class DoSurvey { #answerTables; #firstOpt; #secondOpt; #thirdOpts; constructor() { const { firstOpts, secondOpts, thirdOpts } = new Model().getUserOpts(); this.#firstOpt = getRandomElement(firstOpts); this.#secondOpt = getRandomElement(secondOpts); this.#thirdOpts = thirdOpts; this.#answerTables = [ ...document.querySelectorAll('table.question-wrapper'), ]; } _checkIfHasConfigured() { return this.#firstOpt && this.#secondOpt && this.#thirdOpts.length !== 0; } _firstTypeRun() { const table = this.#answerTables.find((table) => SELECTIONS.first.rawQuestions.some( (q) => q === table.querySelector('tr').innerText, ), ); if (!table) { return; } table .querySelector(SELECTIONS.first.answers[this.#firstOpt].selector) .click(); return; } _secondTypeRun() { const table = this.#answerTables.find((table) => SELECTIONS.second.rawQuestions.some( (q) => q === table.querySelector('tr').innerText, ), ); if (!table) { return; } table .querySelector(SELECTIONS.second.answers[this.#secondOpt].selector) .click(); } _thirdTypeRun() { const questions = document.querySelectorAll(SELECTIONS.third.container); if (questions.length === 0) { return; } questions.forEach((question) => question .querySelector( SELECTIONS.third.answers[getRandomElement(this.#thirdOpts)] .selector, ) .click(), ); } _continueOnWelcome() { const welcomeTable = document.querySelector('table.welcome-table'); if (!welcomeTable) { return; } this._continue(); } // NOTE: This isn't handled strictly _done() { BroadcastSvc.sendDone(); window.close(); } _continue() { const continueBtn = document.querySelector(CONTINUE_BUTTON_SELECTOR) ?? document.querySelector(SUBMIT_BUTTON_SELECT); if (continueBtn) { continueBtn.click(); return; } this._continueOnWelcome(); this._done(); } run() { if (!this._checkIfHasConfigured()) { console.log('Bạn chưa config nên script sẽ không tự điền đâu nhé!'); return; } try { this._firstTypeRun(); this._secondTypeRun(); this._thirdTypeRun(); this._continue(); } catch (e) { console.error(e); throw new Error(e); } } } class AutoRun { #broadCast; #surveys; #current; constructor(surveys) { this.#surveys = surveys; this.#current = 0; window.addEventListener('beforeunload', this._confirmCloseTab); this.#broadCast = new BroadcastSvc(() => this._iterateSurvey()); this._run(); } _iterateSurvey() { this.#current++; if (this.#current < this.#surveys.length) { this._run(); } else { window.removeEventListener('beforeunload', this._confirmCloseTab); GM_notification({ text: 'Đã hoàn thành xong tất cả các khảo sát 😇', title: 'UALS', tag: 'uals-auto_survey_done', timeout: 3000, }); this.#broadCast.removeReceiveMsgListener(); location.reload(); } } _run() { GM_openInTab(this.#surveys[this.#current], { active: false, }); } _confirmCloseTab(e) { e.preventDefault(); e.returnValue = ''; } } class ViewRunAuto { #surveyList; #autoRun; constructor(surveyList) { this.#surveyList = surveyList; this.#autoRun = null; } _startBtnHTML() { return ` <button class="uals__btn" id="uals__run-btn"> Run Auto </button> `; } _stopBtnHTML() { return ` <button class="uals__btn uals__run-btn--running" id="uals__run-btn"> Stop Auto </button> `; } _unavailableBtnHTML() { return ` <button class="uals__btn uals__run-btn--unavailable" id="uals__run-btn" disabled> No Survey </button> `; } btnHTML() { return this.#surveyList.length === 0 ? this._unavailableBtnHTML() : this._startBtnHTML(); } addHandler() { document.querySelector('#uals__run-btn').addEventListener('click', () => { if (this.#autoRun) { GM_notification({ text: 'Bạn đã dùng Auto Run rồi. Hãy refresh trang để refresh', title: 'UALS', tag: 'uals-already_auto_run', timeout: 3000, }); return; } this.#autoRun = new AutoRun(this.#surveyList); }); } } class ViewConfig { #model; constructor(model) { this.#model = model; } btnHTML() { return ` <button class="uals__btn" id="uals__config-btn"> Config </button> `; } _pleaseStarHTML() { return ` <p class="uals__please_star">👆 Cho mình xin 1 star repo dới 👆</p> `; } _saveConfigBtnHTML() { return ` <button class="uals__btn" id="uals__save-config-btn"> Save </button> `; } _resetConfigBtnHTML() { return ` <button class="uals__btn" id="uals__reset-config-btn"> Reset </button> `; } configMenuHTML() { return ` <div id="uals__menu-container"> <section class="uals__question-section"> <h3 id="uals__menu-header">${SELECTIONS.first.question}</h3> <form class="uals__form-select" id="uals__select-1"> ${SELECTIONS.first.answers .map( (opt, index) => ` <input type="checkbox" name="uals__select-1-${index}" id="uals__select-1-${index}" value="${index}"> <label for="uals__select-1-${index}">${opt.label}</label> `, ) .join('')} </form> </section> <section class="uals__question-section"> <h3 id="uals__menu-header">${SELECTIONS.second.question}</h3> <form class="uals__form-select" id="uals__select-2"> ${SELECTIONS.second.answers .map( (opt, index) => ` <input type="checkbox" name="uals__select-2-${index}" id="uals__select-2-${index}" value="${index}" /> <label for="uals__select-2-${index}">${opt.label}</label> `, ) .join('')} </form> </section> <section class="uals__question-section"> <h3 id="uals__menu-header"> ${SELECTIONS.third.question} </h3> <form class="uals__form-select" id="uals__select-3"> ${SELECTIONS.third.answers .map((_, index) => { return ` <input type="checkbox" name="uals__select-3-${index}" id="uals__select-3-${index}" value="${index}" /> <label for="uals__select-3-${index}">Mức ${index + 1}</label> `; }) .join('')} </form> </section> <div class="uals__btn-container"> ${this._resetConfigBtnHTML()} ${this._saveConfigBtnHTML()} </div> </div> `; } toggleConfigMenu() { document .querySelector('#uals__menu-container') .classList.toggle('uals__menu-container--show'); } tickOptsToPage() { Object.values(this.#model.getUserOpts()).forEach( (opts, selectionIndex) => { if (!opts) { return; } opts.forEach((opt) => document .querySelector(`#uals__select-${selectionIndex + 1}-${opt}`) .click(), ); }, ); } _fetchUserOptsFromPage() { function getSelections(CSSSelector) { const element = document.querySelector(CSSSelector); const formData = new FormData(element); return Object.values(Object.fromEntries([...formData])).map((val) => parseInt(val), ); } return { firstOpts: getSelections('#uals__select-1'), secondOpts: getSelections('#uals__select-2'), thirdOpts: getSelections('#uals__select-3'), }; } _saveUserOptsHandler(handler) { handler.addEventListener('click', () => { const userOpts = this._fetchUserOptsFromPage(); this.#model.setUserOpts(userOpts); this.#model.saveUserOpts(); this.toggleConfigMenu(); }); } _resetUserOptsHandler(handler) { handler.addEventListener('click', () => { Model.deleteUserOpts(); location.reload(); }); } addHandlers() { const configBtn = document.querySelector('#uals__config-btn'); configBtn.addEventListener('click', this.toggleConfigMenu); this._saveUserOptsHandler( document.querySelector('#uals__save-config-btn'), ); this._resetUserOptsHandler( document.querySelector('#uals__reset-config-btn'), ); } } class View { #container; #model; #viewRunAuto; #viewConfig; constructor() { this.#container = this._getContainer(); this.#model = new Model(); this.#viewRunAuto = new ViewRunAuto(this._getSurveyURLs()); this.#viewConfig = new ViewConfig(this.#model); } run() { this.#model.addStyles(); this._render(); this._addHandlers(); if (!this.#model.checkUserOptsExist()) { this.#viewConfig.toggleConfigMenu(); } } _getSurveyURLs() { const urls = [...document.querySelectorAll('table a')]; return urls .filter((url) => url.innerHTML.includes('khảo sát về môn học')) .map((url) => url.getAttribute('href')); } _getContainer() { const html = ` <div id="uals__container"> </div> `; const position = document.querySelector('#content .content'); position.insertAdjacentHTML('afterbegin', html); const container = position.querySelector('#uals__container'); return container; } _insertElement(element) { this.#container.insertAdjacentHTML('beforeend', element); } _headerHTML() { return ` <h2 align="center" style="margin: auto;"> <a href="https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey" target="_blank"> UIT - Auto Lecture Survey </a> </h2> `; } _renderConfigMenu() { this._insertElement(this.#viewConfig.configMenuHTML()); } _render() { this._insertElement(this._headerHTML()); const btnContainer = ` <div class="uals__btn-container"> ${this.#viewConfig.btnHTML()} ${this.#viewRunAuto.btnHTML()} </div> `; this._insertElement(btnContainer); this._insertElement(this.#viewConfig.configMenuHTML()); this.#viewConfig.tickOptsToPage(); } _addHandlers() { this.#viewConfig.addHandlers(); this.#viewRunAuto.addHandler(); } } class Controller { #view; constructor() { this.#view = new View(); } run() { this.#view.run(); } } function init() { if (window.location.pathname === '/sinhvien/phieukhaosat') { const controller = new Controller(); controller.run(); } else { try { const doSurvey = new DoSurvey(); doSurvey.run(); } catch (_) { GM_notification({ text: 'Có lỗi trong quá trình làm khảo sát. Vui lòng check log (F12) tại tab phiếu khảo sát (không phải trang chủ) và tạo issue 😞', title: 'UALS', tag: 'uals-error-survey', ondone: () => { GM_openInTab( 'https://github.com/KevinNitroG/UIT-Auto-Lecture-Survey/issues', { active: true, }, ); }, }); } } } try { init(); } catch (e) { console.error(e); Model.deleteUserOpts(); console.log( "Error occurs. Deleted UserScript's storage. Reloading website", ); GM_notification({ text: 'Có lỗi, UALS tự động xoá storage của UALS và reload', title: 'UALS', tag: 'uals-error-reload', timeout: 3000, }); location.reload(); } })();