Skip to content

Commit

Permalink
Implement basic answer tracking
Browse files Browse the repository at this point in the history
WIP
  • Loading branch information
acquitelol committed Sep 12, 2023
1 parent 9271b05 commit 6d746c4
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 147 deletions.
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,18 @@
}
```

- <img src="extension/assets/menu_theme.png" style="width: 1rem"> Provides access to **Themes** by going into **Menu > Themes** at the top right!
- Provides access to **Themes** by going into **Menu > Themes** at the top right!
- Currently the **themes available** include:
- Pink (Default ✓), Purple, Blue, Aqua, Green, Creme, Dusk, Custom
- Pink (Default ✓), Purple, Blue, Aqua, Green, Creme, Custom
<hr />
- *Note:* ***Custom themes allow you to theme every colored variable available in Azalea, ranging from tints to regular colors to text colors. This uses the colors for `Pink` by default because I advocate for pink and believe it is the best color <3.***
<hr />

- <img src="extension/assets/menu_bookwork.png" style="width: 1rem"> Stores **bookwork checks** and automatically submits them if possible (this is *on* by default, and can be disabled from **Menu > Settings**), otherwise shows you the answer to choose the correct option yourself.
- Stores **bookwork checks** and automatically submits them if possible (this is *on* by default, and can be disabled from **Menu > Settings**), otherwise shows you the answer to choose the correct option yourself.

<hr />

- <img src="extension/assets/menu_name.png" style="width: 1rem"> Allows you to anonymize your username by setting it to **Rosie :3** by default (editable from **Menu > Settings**, off by default) and anonymizes your ID to `1nitiat3-cut3-m0d3-f0r-5parx-m4ths`. This aims to make Sparx a place where you cannot be doxxed by showing a screenshot asking for help!

<hr />

- <img src="extension/assets/menu_garden.png" style="width: 1rem"> Adds a button in the **Menu > Settings** allowing you to open garden game. This is meant for if your school disabled times tables minigames and you had progress on Garden Game beforehand, or want to try the game out!
- Allows you to anonymize your username by setting it to **Rosie :3** by default (editable from **Menu > Settings**, off by default). This aims to make Sparx a place where you cannot be doxxed by showing a screenshot asking for help!

<img src="extension/assets/divider.png">

Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Azalea",
"version": "3.2.0",
"version": "3.2.1",
"description": "The cutest SparxMaths bookwork-bypass extension for Chromium ~!",
"manifest_version": 3,
"author": "Rosie",
Expand Down
64 changes: 0 additions & 64 deletions src/core/bookwork.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import handlers from '@handlers';
import utilities from '@utilities';
import patches from '@patches';
import patcher from '@core/patcher';
import bookwork from '@core/bookwork';
import * as hooks from '@core/hooks';

import { Navigation } from '@azalea/utilities';
Expand All @@ -16,7 +15,6 @@ const azalea = {
utilities,
patches,
patcher,
bookwork,
hooks,
navigation: null as Navigation
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ Object.entries(exfiltratedModules).forEach(([name, mdl]) => {


// Use common[mdl] on its own only if you are *100%* sure that the module exists when you `get` any properties.
// If you are loading early, use await lazyModule(() => common[mdl]) to wait for the module to be found.
// If you are loading early, use await lazyDefine(() => common[mdl]) to wait for the module to be found.
export default { exfiltrate, common };
32 changes: 1 addition & 31 deletions src/core/patches/bookworkBypass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,9 @@ import patcher from '@core/patcher';
import utilities from '@utilities';
import { storages } from '@handlers/state';

const { findReact, findInReactTree, lazyModule } = utilities;
const { findReact, findInReactTree, lazyDefine } = utilities;
const { bookwork, preferences } = storages;

export default async function () {
const wacOverlayNode = await lazyModule(() => document.querySelector('.wac-overlay'));
const WACOverlay = findReact(wacOverlayNode);

patcher.after('render', WACOverlay.__proto__, function (_, res) {
if (!this.props.options) return;

const answerRegexp = /[0-9]/g;
const answers = bookwork.get(this.props.bookworkCode);

for (const option of this.props.options) {
const optionMatches = option.get('answerMarkup')?.match(answerRegexp);
const answerMatches = answers?.join('')?.match(answerRegexp);

// If it thinks it has the correct answer, then submit that answer
// This only works for Number-based answers, so images and text won't be submitted automatically
if (optionMatches?.join('')?.includes(answerMatches?.join('')) && preferences.get('autoBookwork')) {
this.props.onSubmitAnswer('', null, option, false);
return res;
}
}

console.warn('Couldn\'t submit answer automatically or answer wasn\'t found!');
const container = findInReactTree(res, r => r.props.children[1].props.className?.includes('bookwork-code'));
if (!container) return;

// Repurpose the components they used
container.props.children[0].props.children = `The answer for '${this.props.bookworkCode}' wasn't submitted automatically and is written below.`;
container.props.children[1].props.children = `Answer: ${answers.join('')} (${this.props.bookworkCode})`;

return res;
})
}
89 changes: 70 additions & 19 deletions src/core/patches/captureAnswers.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,79 @@
import patcher from '@core/patcher';
import bookwork from '@core/bookwork';
import utilities from '@utilities';
import { storages } from '@core/handlers/state';

const { storeAnswers } = bookwork;
const { findReact, lazyModule } = utilities;
const { findReact, findInReactTree, lazyDefine, isEmpty } = utilities;
const { bookwork } = storages;

function processAnswers(input: Record<string, Record<string, any>>) {
const answers = [];

if (!isEmpty(input.number_fields)) {
Object.values(input.number_fields).forEach(field =>
field.value && answers.push(field.value));
}

if (!isEmpty(input.cards)) {
Object.values(input.cards).forEach(card =>
card.slot_ref && answers.push(card.content[0].text))
}

if (!isEmpty(input.choices)) {
Object.values(input.choices).forEach(choice =>
choice.selected && answers.push(choice.content[0].text))
}

return answers;
}

function handler() {
const possibleQuestionWrapper = document.querySelector('[class*="_QuestionWrapper_"]');
const possibleQuestionInfo = document.querySelector('[class*="_QuestionInfo_"]');

if (!possibleQuestionWrapper || !possibleQuestionInfo) {
console.warn('Wrappers failed to query:', { possibleQuestionWrapper, possibleQuestionInfo });
return;
}

const QuestionWrapper = findReact(possibleQuestionWrapper);
const QuestionInfo = findReact(possibleQuestionInfo);

const code = QuestionInfo.memoizedProps.bookworkCode;

const endpoint = findInReactTree(QuestionWrapper.memoizedProps.children, x => x.layout && x.input);
const answer = processAnswers(endpoint.input);

if (!code || !answer) {
console.warn('Answer failed to parse:', { code, answer });
return;
}

bookwork.set(code, answer);
}

const conditions = (event) => {
return [
event.target.className.includes('_Content_10evl_'),
event.target.className.includes('_TextFieldNumeric_'),
event.key === 'Enter'
]
}

export default async function () {
const screenNode = await lazyModule(() => document.querySelector('.screen'));
const SparxWeb = findReact(screenNode);
const page = await lazyDefine(() => document.querySelector('[id="root"]'), undefined, Infinity);

// This will adapt whenever SparxWeb re-renders
let dynamicSubmitButton: Element | null;
console.log("Found page:", page);

// Listen for Enter keypresses and store the answer when the dynamicSubmitButton exists (is in scope)
document.addEventListener('keypress', function (event) {
event.key === 'Enter' && dynamicSubmitButton && storeAnswers();
})
const storeAnswers = (event) => {
if (conditions(event).some(c => c)) {
handler();
}
}

// Assigns `storeAnswers` on click to Submit button on every SparxWeb render
// This then captures the answer(s) in the input whenever the button does exist
patcher.after('render', SparxWeb, function () {
dynamicSubmitButton = document.querySelector('#skill-delivery-submit-button');
page.addEventListener('click', storeAnswers);
page.addEventListener('keydown', storeAnswers);

dynamicSubmitButton?.removeEventListener('click', storeAnswers);
dynamicSubmitButton?.addEventListener('click', storeAnswers);
});
return () => {
page.removeEventListener('click', storeAnswers);
page.removeEventListener('keydown', storeAnswers);
}
}
10 changes: 5 additions & 5 deletions src/core/patches/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import bookworkBypass from './bookworkBypass';
import captureAnswers from './captureAnswers';
import menuButtons from './menuButtons';
import captureAnswers from './captureAnswers';
import bookworkBypass from './bookworkBypass';

const patches = Promise.allSettled([
// await bookworkBypass(),
// await captureAnswers(),
menuButtons()
menuButtons(),
captureAnswers(),
bookworkBypass()
])

export default patches;
6 changes: 3 additions & 3 deletions src/core/patches/menuButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import items from '@patches/menu';
import components from '@core/components';
import { MenuItem } from '@azalea/types';

const { lazyModule, findReact, findInReactTree } = utilities;
const { lazyDefine, findReact, findInReactTree } = utilities;
const { Theming } = handlers;
const { React } = modules.common;

const patches = [];

export default async function () {
const labelNode = await lazyModule(() => document.querySelector('[class*="_XPCount_g7mut_"]'));
const dropdownNode = await lazyModule(() => document.querySelector('[class*="_DropdownMenuContent_"][role="menu"]'), undefined, Infinity);
const labelNode = await lazyDefine(() => document.querySelector('[class*="_XPCount_g7mut_"]'));
const dropdownNode = await lazyDefine(() => document.querySelector('[class*="_DropdownMenuContent_"][role="menu"]'), undefined, Infinity);

const Dropdown = findReact(dropdownNode);

Expand Down
8 changes: 5 additions & 3 deletions src/core/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import common from './common';
import findInReactTree from './findInReactTree';
import findInTree from './findInTree';
import findReact from './findReact';
import lazyModule from './lazyModule';
import lazyDefine from './lazyDefine';
import navigate from './navigate';
import isEmpty from './isEmpty';

const utilities = {
...common,
findInReactTree,
findInTree,
findReact,
lazyModule,
navigate
lazyDefine,
navigate,
isEmpty
}

export default utilities;
14 changes: 14 additions & 0 deletions src/core/utilities/isEmpty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @description Checks if an object is empty or not
* @param {object} object - The object to check
* @return {boolean}
*/
function isEmpty(object: Record<any, any>) {
for (const _ in object) {
return false;
}

return true;
}

export default isEmpty;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* @param {number?} time - The time to wait before trying to get the module from the callback again. The default is 100ms.
* @return {Promise<T | null>} - Returns the module if found or null if max attempts were reached. This must be awaited.
*/
async function lazyModule<T>(callback: () => T, condition?: (result: T | null) => boolean, maxAttempts = 100, time = 100): Promise<T | null> {
async function lazyDefine<T>(callback: () => T, condition?: (result: T | null) => boolean, maxAttempts = 100, time = 100): Promise<T | null> {
let attempt = 0;

while (attempt < maxAttempts) {
Expand All @@ -21,4 +21,4 @@ async function lazyModule<T>(callback: () => T, condition?: (result: T | null) =
return null;
}

export default lazyModule;
export default lazyDefine;
4 changes: 2 additions & 2 deletions src/entry/logo.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import utilities from '@core/utilities';

const { lazyModule, getImage } = utilities;
const { lazyDefine, getImage } = utilities;

async function initializeLogo() {
const sparxLogoContainer = await lazyModule(() => document.querySelector('[class*="_SMLogo_g7mut_"]'));
const sparxLogoContainer = await lazyDefine(() => document.querySelector('[class*="_SMLogo_g7mut_"]'));
const sparxLogo = (sparxLogoContainer.childNodes[0] as HTMLImageElement);

// Apply cuter logo
Expand Down
Loading

0 comments on commit 6d746c4

Please sign in to comment.