Standa Lukeš
4 years ago
4 changed files with 317 additions and 6 deletions
@ -0,0 +1,205 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { isLoggedIn, parseTaskId } from "./ksp-task-grabber"; |
||||
|
import type { TaskStatus } from "./ksp-task-grabber"; |
||||
|
import type { TaskSubmitStatus, SubtaskSubmitStatus } from './ksp-submit-api' |
||||
|
import * as api from './ksp-submit-api' |
||||
|
import { taskStatuses } from './task-status-cache' |
||||
|
|
||||
|
export let id: string; |
||||
|
const taskFromCache: TaskStatus | undefined = $taskStatuses.get(id) |
||||
|
let task: TaskSubmitStatus |
||||
|
let subtaskId: string | null | undefined = null |
||||
|
let uploadSubtaskId: string | null | undefined = null |
||||
|
let expiresInSec: number = 0 |
||||
|
let tick = 0 |
||||
|
let validSubmitSubtasks: SubtaskSubmitStatus[] = [] |
||||
|
let downloading = false |
||||
|
let generating = false |
||||
|
|
||||
|
$: { |
||||
|
if (task && task.id == "cviciste/" + id) { |
||||
|
break $ |
||||
|
} |
||||
|
|
||||
|
task = { |
||||
|
// fill with guesses to prevent flashing |
||||
|
id, |
||||
|
name: "", |
||||
|
points: taskFromCache?.points ?? 0, |
||||
|
max_points: taskFromCache?.maxPoints ?? 1, |
||||
|
subtasks: [ |
||||
|
{ id: "1", input_generated: false, max_points: 1, points: 0 } |
||||
|
] |
||||
|
} |
||||
|
subtaskId = "1" |
||||
|
|
||||
|
api.taskStatus(id) |
||||
|
.then(t => { |
||||
|
task = t |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
$: { |
||||
|
if (!task.subtasks.find(t => t.id == subtaskId)) |
||||
|
subtaskId = task.subtasks.find(t => t.points < t.max_points - 0.001)?.id |
||||
|
} |
||||
|
|
||||
|
$: { |
||||
|
tick |
||||
|
expiresInSec = subtaskId ? calcExpires(subtaskId) : 0 |
||||
|
} |
||||
|
window.setInterval(() => { tick++ }, 1000) |
||||
|
|
||||
|
$: { |
||||
|
tick |
||||
|
validSubmitSubtasks = task.subtasks.filter(t => calcExpires(t.id) > 0) |
||||
|
} |
||||
|
$: { |
||||
|
if (!validSubmitSubtasks.map(t => t.id).includes(uploadSubtaskId!)) { |
||||
|
uploadSubtaskId = validSubmitSubtasks[0]?.id |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function calcExpires(subtask: string): number { |
||||
|
const st = task.subtasks.find(t => t.id == subtask)! |
||||
|
if (!st.input_generated) { |
||||
|
return 0 |
||||
|
} |
||||
|
const validUntil = new Date(st.input_valid_until!).valueOf() |
||||
|
const now = Date.now() |
||||
|
if (validUntil < now) { |
||||
|
return 0 |
||||
|
} else { |
||||
|
return (validUntil - now) / 1000 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function nameSubtask(id: string) { |
||||
|
const map: any = { |
||||
|
"1": "první", |
||||
|
"2": "druhý", |
||||
|
"3": "třetí", |
||||
|
"4": "čtvrtý", |
||||
|
"5": "pátý", |
||||
|
"6": "šestý", |
||||
|
"7": "sedmý", |
||||
|
"8": "osmý", |
||||
|
"9": "devátý", |
||||
|
"10": "desátý" |
||||
|
} |
||||
|
return map[id] ?? id |
||||
|
} |
||||
|
|
||||
|
function magicTrickSaveBlob(blob: Blob, fileName: string) { |
||||
|
const url = URL.createObjectURL(blob) |
||||
|
magicTrickSaveFile(url, fileName) |
||||
|
window.URL.revokeObjectURL(url); |
||||
|
} |
||||
|
function magicTrickSaveFile(url: string, fileName: string) { |
||||
|
var a = document.createElement("a"); |
||||
|
document.body.appendChild(a); |
||||
|
a.style.display = "none"; |
||||
|
a.href = url; |
||||
|
a.download = fileName; |
||||
|
a.click(); |
||||
|
a.remove() |
||||
|
} |
||||
|
|
||||
|
async function download() { |
||||
|
// copy to prevent races |
||||
|
const [id_, subtaskId_] = [id, subtaskId] |
||||
|
if (!subtaskId_) { return } |
||||
|
if (expiresInSec < 20) { |
||||
|
const x = await api.generateInput(id_, subtaskId_) |
||||
|
if (id_ != id) { return } |
||||
|
const subtasks = [...task.subtasks] |
||||
|
subtasks[subtasks.findIndex(s => s.id == x.id)] = x |
||||
|
task = { ...task, subtasks } |
||||
|
} |
||||
|
// It's probably better to download the input using the "old" method |
||||
|
// TODO: specify that as an API |
||||
|
|
||||
|
const parsedId = parseTaskId(id_) |
||||
|
magicTrickSaveFile(`/cviciste/?in=1:sub=${subtaskId}:task=${id_}:year=${parsedId!.rocnik}`, subtaskId_ + ".in.txt") |
||||
|
|
||||
|
// const blob = await api.getInput(id_, subtaskId_) |
||||
|
// magicTrickSaveBlob(blob, subtaskId_ + ".in.txt") |
||||
|
} |
||||
|
|
||||
|
async function upload(file: File) { |
||||
|
const x = await api.submit(id, uploadSubtaskId!, file) |
||||
|
alert(x.verdict) |
||||
|
} |
||||
|
|
||||
|
function fileChange(ev: Event) { |
||||
|
const file = (ev.target as HTMLInputElement).files![0] |
||||
|
upload(file) |
||||
|
} |
||||
|
|
||||
|
function drop(ev: DragEvent) { |
||||
|
ev.preventDefault() |
||||
|
const files = ev.dataTransfer?.files ?? [] |
||||
|
if (files.length > 1) { |
||||
|
alert("Drag & Drop funguje, ale musíš jenom jeden soubor") |
||||
|
} |
||||
|
if (files.length > 0) { |
||||
|
upload(files[0]) |
||||
|
} |
||||
|
} |
||||
|
function dragOver(ev: DragEvent) { |
||||
|
ev.preventDefault() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
|
||||
|
</style> |
||||
|
|
||||
|
<svelte:body on:drop={drop} on:dragover={dragOver} /> |
||||
|
|
||||
|
<div class="odevzdavatko"> |
||||
|
<div class="download"> |
||||
|
<button class="download" |
||||
|
on:click={download} |
||||
|
disabled={subtaskId == null || downloading || generating}> |
||||
|
{#if generating} |
||||
|
Generuji... |
||||
|
{:else if downloading} |
||||
|
Stahuji... |
||||
|
{:else if expiresInSec > 20} |
||||
|
Stáhnout |
||||
|
{:else} |
||||
|
Vygenerovat a stáhnout |
||||
|
{/if} |
||||
|
</button> |
||||
|
<select bind:value={subtaskId}> |
||||
|
<option value={null}></option> |
||||
|
{#each task.subtasks as subtask} |
||||
|
<option value={subtask.id}> |
||||
|
{nameSubtask(subtask.id)} |
||||
|
{#if subtask.points > subtask.max_points - 0.0001} |
||||
|
<span title="Splněno">✔️</span> |
||||
|
{/if} |
||||
|
</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
vstup. |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
{#if validSubmitSubtasks.length > 0} |
||||
|
<div class="upload"> |
||||
|
Odevzdat |
||||
|
<select value={uploadSubtaskId}> |
||||
|
{#each validSubmitSubtasks as subtask} |
||||
|
<option value={subtask.id}> |
||||
|
{nameSubtask(subtask.id)} |
||||
|
</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
vstup: |
||||
|
|
||||
|
<input type="file" on:change={fileChange}> (nebo přetáhni soubor na stránku) |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
@ -0,0 +1,87 @@ |
|||||
|
import { fetchHtml } from "./ksp-task-grabber"; |
||||
|
|
||||
|
let apitoken : string | null = null |
||||
|
|
||||
|
async function getToken(): Promise<string | undefined> { |
||||
|
if (apitoken != null) { |
||||
|
return apitoken |
||||
|
} |
||||
|
let doc = await fetchHtml("/auth/apitoken.cgi?show=1") |
||||
|
const token = Array.from(doc.querySelectorAll("#content p")).map(x => /Aktuální token: (.*)/.exec(x.innerHTML.trim())).filter(x => x != null).map(x => x![1])[0] |
||||
|
if (token) { |
||||
|
return apitoken = token; |
||||
|
} |
||||
|
const form = doc.getElementById("apitoken") as HTMLFormElement |
||||
|
const op = form.elements.namedItem("op")!.value |
||||
|
const submit = form.elements.namedItem("submit")!.value |
||||
|
const csrfToken = form.elements.namedItem("_token")!.value |
||||
|
const body = `op=${encodeURIComponent(op)}&submit=${encodeURIComponent(submit)}&_token=${encodeURIComponent(csrfToken)}` |
||||
|
console.log(`Creating new API token`) |
||||
|
await fetch("/auth/apitoken.cgi", { method: "POST", body, headers: [["Content-Type", "application/x-www-form-urlencoded"]], redirect: "manual" }) |
||||
|
return await getToken() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
async function request(url: string, options: RequestInit = {}): Promise<Response> { |
||||
|
const token = await getToken() |
||||
|
const headers = new Headers(options.headers) |
||||
|
headers.append("Authorization", "Bearer " + token) |
||||
|
const opts: RequestInit = { ...options, headers } |
||||
|
const r = await fetch(url, opts) |
||||
|
if (r.status >= 400) { |
||||
|
throw await r.json() |
||||
|
} |
||||
|
return r |
||||
|
} |
||||
|
|
||||
|
async function requestJson<T>(url: string, options: RequestInit = {}): Promise<T> { |
||||
|
const r = await request(url, options) |
||||
|
return await r.json() |
||||
|
} |
||||
|
|
||||
|
export function listTasks(): Promise<string[]> { |
||||
|
return requestJson("/api/tasks/list?set=cviciste") |
||||
|
} |
||||
|
|
||||
|
export type SubtaskSubmitStatus = { |
||||
|
id: string |
||||
|
points: number |
||||
|
max_points: number |
||||
|
input_generated: boolean |
||||
|
input_valid_until?: string |
||||
|
submitted_on?: string |
||||
|
verdict?: string |
||||
|
} |
||||
|
|
||||
|
export type TaskSubmitStatus = { |
||||
|
id: string |
||||
|
name: string |
||||
|
points: number |
||||
|
max_points: number |
||||
|
subtasks: SubtaskSubmitStatus[] |
||||
|
} |
||||
|
|
||||
|
export function taskStatus(id: string): Promise<TaskSubmitStatus> { |
||||
|
return requestJson(`/api/tasks/status?task=${encodeURIComponent("cviciste/" + id)}`) |
||||
|
} |
||||
|
|
||||
|
export function generateInput(id: string, subtask: string): Promise<SubtaskSubmitStatus> { |
||||
|
return requestJson(`/api/tasks/generate?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`, { method: "POST" }) |
||||
|
} |
||||
|
|
||||
|
export async function getInput(id: string, subtask: string): Promise<Blob> { |
||||
|
const r = await request(`/api/tasks/input?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`) |
||||
|
return await r.blob() |
||||
|
} |
||||
|
|
||||
|
export async function submit(id: string, subtask: string, uploadedData: string | Blob): Promise<SubtaskSubmitStatus> { |
||||
|
return requestJson( |
||||
|
`/api/tasks/submit?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`, |
||||
|
{ |
||||
|
method: "POST", |
||||
|
body: uploadedData, |
||||
|
headers: [ |
||||
|
["Content-Type", "text/plain"] |
||||
|
] |
||||
|
}) |
||||
|
} |
Reference in new issue