Skip to content
This repository was archived by the owner on Jul 27, 2024. It is now read-only.

Commit b14c636

Browse files
committed
refactor course scrape
Course scrape script now utilizes modules, and includes a vue UI
1 parent fa17ec5 commit b14c636

File tree

9 files changed

+559
-395
lines changed

9 files changed

+559
-395
lines changed

Closure_Front_End/src/huji-import/Bookmarklet.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<script>
1212
13-
import scrapeScriptUrl from "./course-scrape.user.js?url"
13+
import scrapeScriptUrl from "./course-scrape-entry.js?url"
1414
1515
export default {
1616
setup() {
@@ -23,7 +23,8 @@ export default {
2323
const url = encodeURI(`javascript:(async function() {
2424
console.log('attaching scrape script and vite HMR client(dev)')
2525
await import('${hmrURL}')
26-
await import('${scrapeScriptFullURL}')
26+
const { setupApp } = await import('${scrapeScriptFullURL}')
27+
setupApp()
2728
})();`)
2829
2930
return {
@@ -37,7 +38,8 @@ export default {
3738
3839
const url = encodeURI(`javascript:(async function() {
3940
console.log('attaching scrape script(production)')
40-
await import('${scrapeScriptFullURL}')
41+
const { setupApp } = await import('${scrapeScriptFullURL}')
42+
setupApp()
4143
})();`)
4244
return {
4345
dev: false, url
Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,122 @@
11
<template>
2-
<h1>Embedded Scrape Status - {{now}} </h1>
2+
<div id="courseScrapeRoot" class="box block has-text-centered" dir="rtl">
3+
<h1 class="title">ייבוא קורסים מהאוניברסיטה</h1>
4+
5+
<div v-if="status === 'initializing'">
6+
<h2 class="subtitle">מאתחל...</h2>
7+
<progress class="progress is-primary max=100" >15%</progress>
8+
</div>
9+
10+
<div v-if="status === 'startedFetching'">
11+
<h2 class="subtitle">מחלץ קורסים מאתר האוניברסיטה</h2>
12+
<progress class="progress is-primary max=100" >15%</progress>
13+
</div>
14+
15+
<div v-if="errors.length > 0">
16+
<div v-for="error in errors" :key="error.message" class="notification is-danger">
17+
<strong>שגיאה:</strong>
18+
<br/>
19+
{{ error.message }}
20+
<br/>
21+
<strong>פרטים טכניים:</strong>
22+
<br/>
23+
{{ error.detail }}
24+
</div>
25+
</div>
26+
27+
<div v-if="warnings.length > 0">
28+
<div v-for="warning in warnings" :key="warning.message" class="notification is-warning">
29+
<strong>אזהרה:</strong>
30+
<br/>
31+
{{ warning.message }}
32+
<br/>
33+
<strong>פרטים טכניים:</strong>
34+
<br/>
35+
{{ JSON.stringify(warning.exception) }}
36+
</div>
37+
</div>
38+
39+
<div v-if="status === 'ready'">
40+
<button class="button is-large is-primary" @click="beginScrapeHandler">התחלה</button>
41+
</div>
42+
43+
<div v-if="status === 'finished'">
44+
<button class="button is-large is-success" @click="continueHandler">המשך מהאתר</button>
45+
</div>
46+
</div>
347
</template>
448

549
<script>
50+
51+
import { reactive, toRefs } from 'vue'
52+
import { install } from './course-scrape-communication.js'
53+
import { warnings, ScrapeError } from './course-scrape-errors.js'
54+
import { fetchCoursesAndGradesDocument, beginScrape } from './course-scrape-logic.js'
55+
56+
657
export default {
7-
data: function() {
8-
return {
9-
now: new Date().toUTCString()
58+
setup() {
59+
60+
const state = reactive({
61+
status: "initializing",
62+
errors: []
63+
})
64+
65+
const nonReactiveState = {
66+
}
67+
68+
const handleException = (ex) => {
69+
console.error(ex)
70+
state.status = "error"
71+
72+
if (ex instanceof ScrapeError) {
73+
state.errors.push({
74+
message: ex.message,
75+
detail: ex.cause.toString()
76+
})
77+
} else {
78+
state.errors.push({
79+
message: "שגיאה כלשהי",
80+
detail: ex.toString()
81+
})
82+
}
83+
}
84+
85+
86+
87+
Promise.all([
88+
fetchCoursesAndGradesDocument(document),
89+
install()
90+
]).then(([gradesDocument, opener]) => {
91+
nonReactiveState.opener = opener
92+
nonReactiveState.gradesDocument = gradesDocument
93+
state.status = "ready"
94+
}, handleException)
95+
96+
const beginScrapeHandler = async () => {
97+
try {
98+
state.status = "startedFetching"
99+
await beginScrape(nonReactiveState.gradesDocument)
100+
state.status = "finished"
101+
} catch (ex) {
102+
handleException(ex)
103+
}
104+
}
105+
106+
const continueHandler = () => {
107+
window.blur()
108+
nonReactiveState.opener.focus()
109+
window.close()
10110
}
111+
112+
return { ...toRefs(state), beginScrapeHandler, warnings, continueHandler }
11113
}
12114
}
13115
</script>
14116

15-
<style>
16-
117+
<style scoped>
118+
@import "https://cdn.jsdelivr.net/npm/[email protected]/css/bulma-rtl.min.css";
119+
#courseScrapeRoot {
120+
margin: 5vh 5vw 5vh 5vw;
121+
}
17122
</style>
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/** This module is responsible for communication from the bookmarklet(injected on HUJI's website)
2+
* into the front-end site(Closure) via `Window.postMessage` API
3+
*
4+
* @module
5+
**/
6+
7+
import { ScrapeError } from "./course-scrape-errors.js"
8+
9+
10+
11+
/** @type {string} */
12+
const FRONTEND_ORIGIN = import.meta.env.VITE_AUTH0_REDIRECT_URI
13+
14+
15+
const HOOKING_TIMEOUT_MS = 3000
16+
17+
18+
19+
export const TRY_HOOK_MESSAGE_TYPE = "tryHook"
20+
export const HOOKED_MESSAGE_TYPE = "hooked"
21+
export const STARTED_MESSAGE_TYPE = "started"
22+
export const GOT_COURSE_MESSAGE_TYPE = "gotCourse"
23+
export const FINISHED_PARSING_MESSAGE_TYPE = "finishedParsing"
24+
25+
26+
27+
/**
28+
* This function tries to initiate communication with the front-end website that
29+
* (supposedly) opened this website, by recursively traversing the chain of all openers
30+
* and sending them a "try hook" message.
31+
*
32+
* This is required because the process of logging into HUJI's personal information website and reaching the personal grades website may involve
33+
* several pop-ups, and only one of them is the front-end.
34+
* Because of cross-origin security, we cannot simply invoke the `opener.origin` property, so we use a basic
35+
* handshaking protocol to determine which opener belongs to the front-end - this is the opener that is returned from
36+
* the 'install' function.
37+
*
38+
*/
39+
function tryHookAllOpeners() {
40+
let opener = window.opener
41+
let level = 0
42+
while (opener) {
43+
level++
44+
try {
45+
console.log(`Trying to communicate with level ${level}`)
46+
opener.postMessage({
47+
type: TRY_HOOK_MESSAGE_TYPE, level
48+
}, FRONTEND_ORIGIN)
49+
} catch (e) {
50+
console.error(`Error while trying to post message: ${e}`)
51+
}
52+
opener = opener.opener
53+
}
54+
console.log(`Finished sending tryHook messages to ${level} windows`)
55+
}
56+
57+
58+
/**
59+
* @param {number} ms Timeout in miliseconds
60+
* @param {() => any} error Error object constructor
61+
* @returns {Promise<void>} A promise that rejects once the specified time passes
62+
*/
63+
function timeout(ms, error) {
64+
return new Promise((resolve, reject) => setTimeout(() => {
65+
reject(error())
66+
}, ms));
67+
}
68+
69+
70+
/** @type {?WindowProxy} */
71+
let feOpener = null
72+
73+
74+
/**
75+
* Installs a message handler that deals with messages from the front-end origin,
76+
* and tries to hook into the front-end.
77+
*
78+
* @returns {Promise<WindowProxy>} A promise that resolves with the window of the front-end origin
79+
* once hooking is successful, or rejects if no
80+
*/
81+
export function install() {
82+
if (feOpener !== null) {
83+
console.warn("Already hooked into font-end, re-hooking.")
84+
feOpener = null
85+
}
86+
87+
const timeoutPromise = timeout(HOOKING_TIMEOUT_MS,
88+
() => new ScrapeError(
89+
"האם הרצת את הסקריפט מהחלון הנכון, אשר נפתח דרך אתר Closure?",
90+
new Error(`Did not receive hook message from frontend within ${HOOKING_TIMEOUT_MS} miliseconds`)
91+
)
92+
)
93+
94+
const hookedPromise = new Promise(resolve => {
95+
/** @type {(event: MessageEvent) => void }*/
96+
const messageHandler = (event) => {
97+
if (event.origin != FRONTEND_ORIGIN) {
98+
console.warn(`got a message from an unknown origin ${event.origin}, expected origin ${FRONTEND_ORIGIN}`)
99+
return
100+
}
101+
if (event.data?.type === HOOKED_MESSAGE_TYPE) {
102+
console.log(`Hooked into frontend`)
103+
feOpener = event.source
104+
resolve(event.source)
105+
window.removeEventListener("message", messageHandler)
106+
} else {
107+
console.error(`Got unexpected message from front-end before hooking: ${JSON.stringify(event)}`)
108+
}
109+
110+
}
111+
window.addEventListener("message", messageHandler)
112+
})
113+
114+
tryHookAllOpeners()
115+
return Promise.race([timeoutPromise, hookedPromise])
116+
117+
}
118+
119+
120+
function getOpener() {
121+
if (!feOpener) {
122+
throw new Error("Frontend opener isn't defined, must wait for install() to succeed before posting messages")
123+
}
124+
return feOpener
125+
}
126+
127+
export function postStarted() {
128+
const opener = getOpener()
129+
opener.postMessage({ type: STARTED_MESSAGE_TYPE }, FRONTEND_ORIGIN)
130+
}
131+
132+
export function postParsedEntry(course) {
133+
const opener = getOpener()
134+
opener.postMessage({
135+
type: GOT_COURSE_MESSAGE_TYPE,
136+
course
137+
}, FRONTEND_ORIGIN)
138+
}
139+
140+
export function postFinishedParsing() {
141+
const opener = getOpener()
142+
opener.postMessage({ type: FINISHED_PARSING_MESSAGE_TYPE }, FRONTEND_ORIGIN)
143+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createApp } from 'vue'
2+
import EmbeddedScrapeStatus from '@/huji-import/EmbeddedScrapeStatus.vue'
3+
4+
5+
const containerId = "vue-container"
6+
7+
/** Entry point to the bookmarklet, sets up the Vue UI, driving
8+
* the rest of the process.
9+
*/
10+
export function setupApp() {
11+
const preexisting = document.getElementById(containerId)
12+
if (preexisting) {
13+
preexisting.remove()
14+
}
15+
const div = document.createElement("div")
16+
div.id = containerId
17+
document.body.prepend(div)
18+
console.log("setting up ui")
19+
const app = createApp(EmbeddedScrapeStatus).mount(`#${containerId}`)
20+
window.app = app
21+
console.log(`ui is set`)
22+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { reactive, shallowReadonly} from 'vue'
2+
3+
4+
const allWarnings = reactive([])
5+
6+
/** A critical error that may be thrown during scraping */
7+
export class ScrapeError extends Error {
8+
constructor(message, cause) {
9+
super(message)
10+
this.cause = cause
11+
this.name = 'ScrapeError'
12+
}
13+
}
14+
15+
/**
16+
* Reports a warning to console
17+
* @param {string} message
18+
* @param {...any} args Other arguments
19+
*/
20+
export function warning(message, ...args) {
21+
console.warn(message, ...args)
22+
allWarnings.push({ message, ...args})
23+
}
24+
25+
export const warnings = shallowReadonly(allWarnings)

0 commit comments

Comments
 (0)