-
Notifications
You must be signed in to change notification settings - Fork 0
/
stripcode-auto-quiz.js
366 lines (287 loc) · 11.8 KB
/
stripcode-auto-quiz.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
// ==UserScript==
// @name StripCode Cheater
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Cheat my way to rank #0
// @author m.thompson.code@gmail.com
// @match https://stripcode.dev/ranked
// @grant none
// ==/UserScript==
// Used to read and write json with stripcode answerBank
let fileHandle;
let answerBank = {};
let lastCodeSeen = "";
let lastCorrectedCode = "";
(function() {
'use strict';
// FileHandle API requires user action to work
// To get around this, we add a button and click it to start the script
injectStartButton();
})();
// Inject start button on the lower right corner of the screen
function injectStartButton() {
const button = document.createElement('button');
button.innerText = 'Start cheating';
button.className = `mb-4 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none`;
button.style.position = 'fixed';
button.style.zIndex = 1234567890;
button.style.bottom = '12px';
button.style.right = '12px';
button.onclick = () => {
main();
// Hide button after clicking it to avoid clicking it again
button.style.display = 'none';
}
document.body.append(button);
}
async function main() {
// Start FileHandle API
await setFileHandle();
// Get FileHandle permissions
await verifyPermission(fileHandle, true);
// Get answer bank from local file
answerBank = await getFileData();
// Loop getState()
updateStateLoop();
}
let loopTimeout;
async function updateStateLoop() {
await getState();
const finishedText = document.getElementsByClassName('mt-8 text-lg')?.[0]?.innerText;
if (finishedText === 'You finished all the questions for this programming language.') {
console.log("Finished!");
return;
}
clearTimeout(loopTimeout);
loopTimeout = setTimeout(() => {
updateStateLoop();
}, 0);
}
async function getState() {
// Get filename, code (code snippet related to file), get answer buttons
const filename = getFilename();
if (!filename) {
console.warn("no filename found");
return;
}
const code = (getCode() || '').trim();
if (!code) {
console.warn("no code found");
return;
}
const answers = getAnswers();
if (!answers || !answers.length) {
console.warn("no answers found");
return;
}
// END Get filename, code (code snippet related to file), get answer buttons
// Check for a stored correct answers in answerBank
const correctAnswerDatas = getCorrectAnswerDatas(filename, code, answers);
// Search for next question button and click it at some point
const nextButton = getNextQuestionButton();
// If the next question button is visible, this should mean that an answer has been selected
// This also means that the ui should show which answer is correct or incorrect
if (nextButton) {
// Check for incorrect answers if they were chosen from our answerBank
const uiSaysAnswerIsIncorrect = document.getElementsByClassName('text-red-900')?.[0];
// const uiSaysAnswerIsCorrect = document.getElementsByClassName('text-green-900')?.[0];
// Check the ui of the app for which answer is correct (the app shows the correct answer after choosing any answer)
const answerOnScreen = findCorrectAnswer(answers);
// const existingEntry = answerBank.find(entry => entry.filename === filename && entry.correctAnswer === answerOnScreen.text);
answerBank[filename] = answerBank[filename] || {};
answerBank[filename][answerOnScreen.text] = answerBank[filename][answerOnScreen.text] || [];
const existingEntries = answerBank[filename][answerOnScreen.text];//.find(entry => entry.filename === filename && entry.correctAnswer === answerOnScreen.text);
if (filename === answerOnScreen.text) {
debugger;
throw new Error("filename shouldnt match repo");
}
// Update existing entry if the incorrect answer was selected
let updatedAnswerBank = false;
let replaced = false;
for (let i = 0; i < existingEntries.length; i++) {
const existingEntry = existingEntries[i];
if (code.includes(existingEntry) || existingEntry.includes(code)) {
replaced = true;
if (code.length > existingEntry.length) {
existingEntries[i] = code;
await saveAnswerBank(answerBank);
updatedAnswerBank = true;
console.log(" ~ updated entry to answer bank", { filename, text: answerOnScreen.text, code });
lastCorrectedCode = code;
}
}
}
if (!replaced) {
existingEntries.push(code);
await saveAnswerBank(answerBank);
updatedAnswerBank = true;
console.log(" ~ added new entry to answer bank", { filename, text: answerOnScreen.text, code });
lastCorrectedCode = code;
}
if (uiSaysAnswerIsIncorrect) {
if (lastCorrectedCode !== code) {
console.error("Got a wrong answer");
// console.error(answers, correctAnswerDatas);
console.error(answers.map(a => a.text), correctAnswerDatas.slice(0));
console.error(filename, answerOnScreen.text);
if (!updatedAnswerBank) {
debugger;
}
}
}
lastCodeSeen = code;
nextButton.click();
return;
} else {
lastCorrectedCode = "";
}
// If there's more than one possible correct answer based on the answer bank, skip picking one
if (correctAnswerDatas.length > 1) {
if (lastCodeSeen !== code) {
console.warn('found more than one possible answer for code snippet', correctAnswerDatas);
}
// If there's just one possible correct answer based on the answer bank pick that one
} else if (correctAnswerDatas.length === 1) {
const correctAnswerData = correctAnswerDatas[0];
let matchingAnswer;
// Validate if correctAnswerData was actually correct
try {
// Find the correct answer
matchingAnswer = answers.find(answer => answer.text === correctAnswerData).element;
// matchingAnswer = answers.find(answer => answer.text === correctAnswerData.correctAnswer).element;
// Click the correct answer
matchingAnswer.click();
} catch(error) {
// We expect this to error with element of undefined if it wasn't found
console.error(error);
debugger;
}
// If there's no possible answer in the answer bank, just pick the first answer
} else if (!correctAnswerDatas.length) {
// Just click any answer since we don't know which answer is the right one
answers[0].element.click();
console.warn("Clicking first answer since we don't have an entry in answer bank");
}
lastCodeSeen = code;
}
// Search ui of app for the filename
function getFilename() {
return (document.getElementsByClassName('text-4xl')?.[0]?.innerText || '').trim();
}
// Search ui of app for the code snippet
function getCode() {
return (document.getElementsByClassName('code-half')?.[0].getElementsByTagName('pre')?.[0].innerText || '').trim();
}
// Search ui of app for answers, store the text, button element, correct/incorrect state
function getAnswers() {
const answerElesContainer = document.getElementsByClassName('answer-half')?.[0];
if (!answerElesContainer) {
console.warn("no answer elements container found");
return;
}
const answerEles = answerElesContainer.getElementsByClassName('py-4');
return Array.from(answerEles).map(ele => {
const isCorrect = ele.classList.contains('bg-green-100');
return {
element: ele,
text: (ele.getElementsByClassName('text-bblack')[0]?.innerText || ''),
// correct css includes the css for incorrect (will have both the green and red class)
isCorrect: isCorrect,
// Only set the state as incorrect if it's not correct for this reason
isIncorrect: !isCorrect && ele.classList.contains('bg-red-100'),
};
});
}
// Check answers for the correct answer based on the ui
function findCorrectAnswer(answers) {
return answers.find(answer => answer.isCorrect);
}
// Check answerBank for a correct answer that has matching filename, code snippet, and correctAnswer is included on the answers
// It's important to check if the correctAnswer is included with answers since there are duplicate filenames + code snippets
function getCorrectAnswerDatas(filename, code, answers) {
const filenameEntries = answerBank[filename] || {};
const correctAnswerDatas = [];
for (const answer of answers) {
const codeSnippets = filenameEntries[answer.text] || [];
for (const codeSnippet of codeSnippets) {
if (codeSnippet.includes(code)) {
correctAnswerDatas.push(answer.text);
break;
}
}
}
return correctAnswerDatas;
}
// Search ui of app for the next question button
function getNextQuestionButton() {
const answerElesContainer = document.getElementsByClassName('answer-half')?.[0];
const button = answerElesContainer?.getElementsByClassName('text-white')?.[0];
return button;
}
// File Handler API helpers
// source: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission
async function verifyPermission(fileHandle, withWrite) {
const opts = {};
if (withWrite) {
opts.mode = 'readwrite';
}
// Check if we already have permission, if so, return true.
if (await fileHandle.queryPermission(opts) === 'granted') {
return true;
}
// Request permission to the file, if the user grants permission, return true.
if (await fileHandle.requestPermission(opts) === 'granted') {
return true;
}
// The user did not grant permission, return false.
return false;
}
async function setFileHandle() {
// Allow for json files (which should be an array)
const pickerOpts = {
types: [
{
description: 'JSON',
accept: {
'application/json': ['.json']
}
},
],
excludeAcceptAllOption: true,
multiple: false,
};
fileHandle = (await window.showOpenFilePicker(pickerOpts))[0];
}
async function getFileData() {
// get answerBank data
const fileData = await fileHandle.getFile();
// Used FileReader to transform raw data to string -> json object
const fr = new FileReader();
return new Promise(resolve => {
fr.addEventListener("load", e => {
// Default value will be empty in case there's an error
let jsonMap = {};
try {
jsonMap = JSON.parse(fr.result);
} catch(error) {
// This can error if the file is empty, default to an empty
console.error(error);
}
// Update the global answerBank variable
answerBank = jsonMap;
resolve(jsonMap);
});
fr.readAsText(fileData);
});
}
// Use FileHandle API to update local answerBank file
async function saveAnswerBank(content) {
const serializedContent = JSON.stringify(content, undefined, 2);
// create a FileSystemWritableFileStream to write to
const writableStream = await fileHandle.createWritable();
// write our file
await writableStream.write(serializedContent);
// close the file and write the answerBank to disk.
await writableStream.close();
}
// END File Handler API helpers