diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0321e9a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## v1.0.0 + +- Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..feb9ccb --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +

Mortarboard icons created by itim2101 - Flaticon

+ +# Moodle-gpt + +This extension allows you to hide CHAT-GPT in a Moodle quiz. You just need to enter the code configured in the extension on the keyboard and then click on the question you want to solve, and CHAT-GPT will automatically provide the answer. However, one needs to be careful because as we know, CHAT-GPT can make errors especially in calculations. + +## Disclaimer ! + +I hereby declare that I am not responsible for any misuse or illegal activities carried out using my program. The code is provided for educational and research purposes only, and any use of it outside of these purposes is at the user's own risk. + +## Update + +- See [changelog](./CHANGELOG.md) + +## Set up + +Go to "Manage my extensions" on your browser, then click on "Load unpacked extension" and select the "extension" folder. Afterwards, click on the extension icons and enter the apiKey obtained from [openai](https://platform.openai.com/). Finally, enter a code that will activate the extension on your moodle page and enter the langage of the quiz. + +## Test + +To test the code, you can run the index.js file located in the "test" folder. + +## Options + +![Popup](./assets/popup.png) + +- Title indication: Show some informations into the title to know for example if the code have been injected. ![Injected](./assets/title-injected.png) +- Console logs: show logs into the console. ![Logs](./assets/logs.png) +- Cursor indication: show a pointer cursor and a hourglass to know when the request is finished. +- Typing effect: create a typing effect for text. ![Typing](./assets/typing.gif) + +- Mouseover effect: you will need to hover (or click for select) the question response to complete it automaticaly. ![Mouseover](./assets/mouseover.gif) ![Mouseover2](./assets/mouseover2.gif) + +## Examples + +### Select + +![Select](./assets/select.gif) + +### Put in order question + +![Order](./assets/order.gif) + +### Resolve equation + +![Equations](./assets/equations.gif) + +### One response (radio button) + +![Radio](./assets/radio.gif) + +### Multiples responses (checkbox) + +![Checkbox](./assets/checkbox.gif) + +### True or false + +![True-false](./assets/true-false.gif) + +### Text + +![Text](./assets/text.gif) + +## If it can't complete the question, the answer will be copied to your clipboard + +To know if the answer has been copied to the clipboard, you can look at the title of the page which will become "Copied to clipboard" for 5 seconds. + +![Clipboard](./assets/clipboard.gif) diff --git a/assets/checkbox.gif b/assets/checkbox.gif new file mode 100644 index 0000000..90acf7b Binary files /dev/null and b/assets/checkbox.gif differ diff --git a/assets/clipboard.gif b/assets/clipboard.gif new file mode 100644 index 0000000..d1adcde Binary files /dev/null and b/assets/clipboard.gif differ diff --git a/assets/equations.gif b/assets/equations.gif new file mode 100644 index 0000000..69a2c2c Binary files /dev/null and b/assets/equations.gif differ diff --git a/assets/logs.png b/assets/logs.png new file mode 100644 index 0000000..fc0b8cd Binary files /dev/null and b/assets/logs.png differ diff --git a/assets/mouseover.gif b/assets/mouseover.gif new file mode 100644 index 0000000..5e1ed7d Binary files /dev/null and b/assets/mouseover.gif differ diff --git a/assets/mouseover2.gif b/assets/mouseover2.gif new file mode 100644 index 0000000..a06a8f4 Binary files /dev/null and b/assets/mouseover2.gif differ diff --git a/assets/order.gif b/assets/order.gif new file mode 100644 index 0000000..ad168dd Binary files /dev/null and b/assets/order.gif differ diff --git a/assets/popup.png b/assets/popup.png new file mode 100644 index 0000000..f73dbf6 Binary files /dev/null and b/assets/popup.png differ diff --git a/assets/radio.gif b/assets/radio.gif new file mode 100644 index 0000000..62f827e Binary files /dev/null and b/assets/radio.gif differ diff --git a/assets/select.gif b/assets/select.gif new file mode 100644 index 0000000..ce98865 Binary files /dev/null and b/assets/select.gif differ diff --git a/assets/text.gif b/assets/text.gif new file mode 100644 index 0000000..6cad81a Binary files /dev/null and b/assets/text.gif differ diff --git a/assets/title-injected.png b/assets/title-injected.png new file mode 100644 index 0000000..69aff4d Binary files /dev/null and b/assets/title-injected.png differ diff --git a/assets/true-false.gif b/assets/true-false.gif new file mode 100644 index 0000000..4188a59 Binary files /dev/null and b/assets/true-false.gif differ diff --git a/assets/typing.gif b/assets/typing.gif new file mode 100644 index 0000000..d2a2bfd Binary files /dev/null and b/assets/typing.gif differ diff --git a/extension/fonts/Segoe UI.ttf b/extension/fonts/Segoe UI.ttf new file mode 100644 index 0000000..46b3b99 Binary files /dev/null and b/extension/fonts/Segoe UI.ttf differ diff --git a/extension/icon.png b/extension/icon.png new file mode 100644 index 0000000..9402a78 Binary files /dev/null and b/extension/icon.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..13a3061 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 3, + "name": "moddle-gpt", + "version": "1.0", + "description": "Hidden chat-gpt for moodle cheat", + "permissions": ["activeTab", "tabs", "storage"], + "action": { + "default_icon": "icon.png", + "default_popup": "./popup/index.html" + }, + + "icons": { + "16": "icon.png", + "32": "icon.png", + "48": "icon.png", + "128": "icon.png" + }, + + "content_scripts": [ + { + "matches": ["https://*/*", "http://*/*"], + "js": ["moodle-gpt.js"], + "run_at": "document_end" + } + ] +} diff --git a/extension/moodle-gpt.js b/extension/moodle-gpt.js new file mode 100644 index 0000000..cb51479 --- /dev/null +++ b/extension/moodle-gpt.js @@ -0,0 +1,254 @@ +chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { + const config = storage.moodleGPT; + + if (!config) throw new Error("Please configure MoodleGPT into the extension"); + + //listening to the keys to inject moodleGPT + const pressedKeys = []; + const listeners = []; + document.body.addEventListener("keypress", function (e) { + pressedKeys.push(e.key); + if (pressedKeys.length > config.code) pressedKeys.shift(); + if (pressedKeys.join("") === config.code) { + pressedKeys.length = 0; + setUpMoodleGpt(); + } + }); + + /** + * Show some informations into the document title and remove it after 3000ms + * @param {*} text + */ + function titleIndications(text) { + const backTitle = document.title; + document.title = text; + setTimeout(() => (document.title = backTitle), 3000); + } + + /** + * Setup moodleGPT into the page (remove/injection) + */ + function setUpMoodleGpt() { + //removing events + if (listeners.length > 0) { + for (const listener of listeners) { + if (config.cursor) listener.element.style.cursor = "initial"; + listener.element.removeEventListener("click", listener.fn, { + once: true, + }); + } + if (config.title) titleIndications("Removed"); + listeners.length = 0; + return; + } + + //injection + const inputQuery = ["checkbox", "radio", "text"] + .map((e) => `input[type="${e}"]`) + .join(","); + const query = inputQuery + ", textarea, select"; + const forms = Array.from(document.querySelectorAll(".formulation")); + + for (const form of forms) { + const hiddenButton = form.querySelector(".qtext"); + if (config.cursor) hiddenButton.style.cursor = "pointer"; + const fn = reply.bind(null, hiddenButton, form, query); + listeners.push({ element: hiddenButton, fn }); + hiddenButton.addEventListener("click", fn, { once: true }); + } + + if (config.title) titleIndications("Injected"); + } + + /** + * Normlize text + * @param {*} text + */ + function normalizeText(text) { + return text.replace(/\n+/gi, "\n").toLowerCase().trim(); + } + + /** + * Get the response from chatGPT api + * @param {*} question + * @returns + */ + async function getChatGPTResponse(question) { + const req = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.apiKey}`, + }, + body: JSON.stringify({ + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: question }], + temperature: 0.8, + top_p: 1.0, + presence_penalty: 1.0, + stop: null, + }), + }); + const rep = await req.json(); + const response = rep.choices[0].message.content; + return normalizeText(response); + } + + /** + * Handling logs into the console + */ + class Logs { + static question(text) { + const css = "color: blue"; + console.log("%c[QUESTION]: %s", css, text); + } + + static responseTry(text, valide) { + const css = "color: " + (valide ? "green" : "red"); + console.log("%c[CHECKING]: %s", css, text); + } + + static array(arr) { + console.log("[CORRECTS] ", arr); + } + + static response(text) { + console.log(text); + } + } + + /** + * Reply to the question + * @param {*} form + * @param {*} query + * @returns + */ + async function reply(hiddenButton, form, query) { + if (config.cursor) hiddenButton.style.cursor = "wait"; + + const question = normalizeText( + form.textContent.replace("Texte de la question", "") + ); + const inputList = form.querySelectorAll(query); + const response = await getChatGPTResponse( + `Give a short response as possible for this question, reply in this langage "${config.langage}", only show the result: + ${question} + (if you have to choose between multiple results only show the corrects one)` + ); + + if (config.logs) { + Logs.question(question); + Logs.response(response); + } + + if (config.cursor) hiddenButton.style.cursor = "initial"; + + //if we dont find the input we copy into the clipboard + if (inputList.length === 0) { + if (config.title) titleIndications("Copied to clipboard"); + navigator.clipboard.writeText(response); + return; + } + + //if it's a text + if ( + (inputList.length === 1 && inputList[0].type === "text") || + inputList[0].tagName === "TEXTAREA" + ) { + if (config.typing) { + for (let i = 0; i < response.length; ++i) { + setTimeout( + () => (inputList[0].value = response.slice(0, i + 1)), + i * 50 + ); + } + } else { + inputList[0].value = response; + } + return; + } + + //if it's a select + if (inputList[0].tagName === "SELECT") { + let correct = response.split("\n"); + if (correct.length === 1 && correct.length !== inputList) + correct = response.split(","); + + if (config.logs) Logs.array(correct); + + for (let j = 0; j < inputList.length; ++j) { + const options = inputList[j].querySelectorAll("option"); + + for (const option of options) { + const content = option.textContent.toLocaleLowerCase().trim(); + const valide = correct[j].includes(content); + + //if it's a put in order + if (!isNaN(parseInt(content))) { + const content = normalizeText( + option.parentNode.closest("tr").querySelector(".text").textContent + ); + const index = correct.findIndex((c) => { + const valide = c.includes(content); + if (config.logs) Logs.responseTry(content, valide); + return valide; + }); + if (index !== -1) { + if (config.mouseover) { + options[index + 1].closest("select").addEventListener( + "click", + function () { + options[index + 1].selected = "selected"; + }, + { once: true } + ); + } else { + options[index + 1].selected = "selected"; + } + break; + } + } + + if (config.logs) Logs.responseTry(content, valide); + if (valide) { + if (config.mouseover) { + option.closest("select").addEventListener( + "click", + function () { + option.selected = "selected"; + }, + { once: true } + ); + } else { + option.selected = "selected"; + } + break; + } + } + } + return; + } + + //if it's a radio button or checkbox + for (const input of inputList) { + const content = normalizeText(input.parentNode.textContent); + const valide = response + .replace(/^[a-z\d]\.\s/gi, "") + .includes(content.replace(/^[a-z\d]\.\s/gi, "")); + if (config.logs) Logs.responseTry(content, valide); + if (valide) { + if (config.mouseover) { + input.addEventListener( + "mouseover", + function (event) { + event.target.checked = true; + }, + { once: true } + ); + } else { + input.checked = true; + } + } + } + } +}); diff --git a/extension/popup/index.html b/extension/popup/index.html new file mode 100644 index 0000000..5797cc7 --- /dev/null +++ b/extension/popup/index.html @@ -0,0 +1,60 @@ + + + + + + + MoodleGPT + + + + + +
+ icon +

MoodleGPT

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

+
+ +
+
+ + diff --git a/extension/popup/index.js b/extension/popup/index.js new file mode 100644 index 0000000..bae5a3c --- /dev/null +++ b/extension/popup/index.js @@ -0,0 +1,63 @@ +const saveBtn = document.querySelector(".save"); +const message = document.querySelector("#message"); + +const inputsText = ["apiKey", "code", "langage"]; +const inputsCheckbox = ["logs", "title", "cursor", "typing", "mouseover"]; + +function showMessage(messageTxt, valide) { + message.style.color = valide ? "limegreen" : "red"; + message.textContent = messageTxt; + message.style.display = "block"; + setTimeout(() => (message.style.display = "none"), 5000); +} + +//save the configuration +saveBtn.addEventListener("click", function () { + const [apiKey, code, langage] = inputsText.map((selector) => + document.querySelector("#" + selector).value.trim() + ); + const [logs, title, cursor, typing, mouseover] = inputsCheckbox.map( + (selector) => document.querySelector("#" + selector).checked + ); + + if (!apiKey || !code || !langage) { + showMessage("Please comple all the form"); + return; + } + + if (code.length < 3) { + showMessage("The code should at least contain 3 characters"); + return; + } + + chrome.storage.sync.set({ + moodleGPT: { + apiKey, + code, + langage, + logs, + title, + cursor, + typing, + mouseover, + }, + }); + + showMessage("Configuration saved", true); +}); + +//we load back the configuration +chrome.storage.sync.get(["moodleGPT"]).then(function (storage) { + if (storage.moodleGPT) { + const config = storage.moodleGPT; + inputsText.forEach( + (key) => (document.querySelector("#" + key).value = config[key] || "") + ); + inputsCheckbox.forEach( + (key) => (document.querySelector("#" + key).checked = config[key] || "") + ); + } else { + //set default config + document.querySelector("#langage").value = navigator.language; + } +}); diff --git a/extension/popup/style.css b/extension/popup/style.css new file mode 100644 index 0000000..8a40cf8 --- /dev/null +++ b/extension/popup/style.css @@ -0,0 +1,89 @@ +@font-face { + font-family: Segeo UI; + src: url(../../fonts/Segoe\ UI.ttf); +} + +:root { + --bg-color: #121212; + --color: #fff; + --btn-color: #7f39fb; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; + font-family: "Segeo UI", sans-serif; + color: var(--color); +} + +body { + min-height: 100vh; + background-color: var(--bg-color); + display: flex; + justify-content: center; + align-items: center; +} + +main { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.75rem; + gap: 0.4rem; + text-align: center; + width: 20rem; +} + +img { + width: 5rem; + margin-top: 1rem; +} + +h1 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.line { + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} + +.line .textLabel { + width: 5rem; + text-transform: uppercase; +} + +.line input[type="text"], +.line input[type="password"] { + flex: 1 1; + border: thin solid var(--color); + padding: 0.3rem 0.5rem; + border-radius: 0.2rem; + outline-color: transparent; + background-color: transparent; +} + +.line input[type="checkbox"] { + accent-color: var(--btn-color); + margin-right: 0.3rem; +} + +.save { + border: none; + background-color: var(--btn-color); + margin-top: 1.5rem; + padding: 0.5rem 2rem; + cursor: pointer; + border-radius: 0.2rem; + font-size: 1.5rem; + margin-bottom: 1rem; +} + +#message { + display: none; + margin-top: 1rem; +} diff --git a/reset-moodle-inputs/reset.js b/reset-moodle-inputs/reset.js new file mode 100644 index 0000000..6d5d10a --- /dev/null +++ b/reset-moodle-inputs/reset.js @@ -0,0 +1,15 @@ +//to try in real moodle env + +for (const option of document.querySelectorAll("option")) { + option.selected = false; +} + +for (const input of document.querySelectorAll( + 'input[type="radio"], input[type="checkbox"]' +)) { + input.checked = false; +} + +for (const icon of document.querySelectorAll(".text-danger, .text-success")) { + icon.remove(); +} diff --git a/test/css/style.css b/test/css/style.css new file mode 100644 index 0000000..ac8c39d --- /dev/null +++ b/test/css/style.css @@ -0,0 +1,67 @@ +@font-face { + font-family: Segeo UI; + src: url(../../extension/fonts/Segoe\ UI.ttf); +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; + font-family: "Segeo UI"; +} + +body { + display: flex; + align-items: center; + flex-direction: column; + padding: 1rem; + gap: 1rem; +} + +h1 { + text-align: center; +} + +.formulation { + width: 60%; + background-color: #e7f3f5; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.inp { + margin-top: 0.5rem; + display: flex; + gap: 0.5rem; + align-items: center; + position: relative; +} + +select { + position: absolute; + right: 0; +} + +.editable { + background-color: #fff; + height: 10rem; + width: 100%; + resize: vertical; + overflow-y: auto; + white-space: pre-wrap; + padding: 0.5rem; + outline: none; + border: thin solid #000; +} + +textarea { + outline: none; + padding: 0.5rem; + height: 10rem; + width: 100%; + resize: vertical; + white-space: pre-wrap; + overflow-y: auto; +} diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..8c96863 --- /dev/null +++ b/test/index.html @@ -0,0 +1,246 @@ + + + + + + + Moodle test + + + + +
+
+

Which words are animals ?

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

What is the french president name ?

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

The cat sometimes drink milk?

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+

Choose the correct answer

+
+
+
+ + + + + + + + + + + + + + +
+

I am a feline

+
+ +
+

I am a descendant of the wolf

+
+ +

I produce milk

+ +
+
+
+
+ + +
+
+

Put the following tags in the order of creation of a html file:

+
+
+
+ + + + + + + + + + + + + + +
+

html

+
+ +
+

body

+
+ +

head

+ +
+
+
+
+ + +
+
+

Solve those equations:

+
+
+
+ + + + + + + + + + + + + + +
+

5*5

+
+ +
+

20 - 10

+
+ +

10+10

+ +
+
+
+
+ + +
+
+

Give me five diferences between a dog and a cat

+
+
+ +
+
+ + +
+
+

+ Gives a "reverseWorld" function in javascript which takes as a + parameter a word and flips it in the opposite direction +

+
+
+
+
+
+ +