263 lines
8.5 KiB
TypeScript
263 lines
8.5 KiB
TypeScript
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
|
|
import staticFiles from "https://deno.land/x/static_files@1.1.6/mod.ts";
|
|
import * as rawQuestions from "./questions.js";
|
|
import { renderToString } from "https://esm.sh/preact-render-to-string@5.1.19?deps=preact@10.5.15";
|
|
import * as prand from 'https://unpkg.com/pure-rand/lib/esm/pure-rand.js';
|
|
import {QuestionSource, QuestionMeta} from './shared.ts';
|
|
|
|
import highlightJs from "https://esm.sh/highlight.js@11.9.0"
|
|
import highlightJsJavascript from "https://esm.sh/highlight.js@11.9.0/es/languages/javascript.js"
|
|
highlightJs.registerLanguage('javascript', highlightJsJavascript);
|
|
|
|
type Question = [QuestionSource, QuestionMeta];
|
|
|
|
type QuestionURLS = {
|
|
nextQuestionUrl: string,
|
|
validationUrl: string,
|
|
answerQuestionUrl: string
|
|
};
|
|
|
|
const questions: Question[] = Object.values(rawQuestions).flatMap(s => s.questions.map( (q, idx) => [s as QuestionSource, q as QuestionMeta]));
|
|
|
|
const apiRouter = new Router();
|
|
const loginRouter = new Router();
|
|
const app = new Application();
|
|
|
|
function hashString(str: string): number {
|
|
let hash = 0
|
|
for (let i = 0; i < str.length; i++) {
|
|
const chr = str.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + chr;
|
|
hash |= 0; // Convert to 32bit integer
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
|
|
function randomPermutation(seed: number, n: number): number[] {
|
|
const rand = prand.mersenne(seed)
|
|
const base = [...Array(n).keys()];
|
|
for(let i = 0; i < (n -1); i++){
|
|
const j = prand.unsafeUniformIntDistribution(i, n-1, rand);
|
|
const tmp = base[i];
|
|
base[i] = base[j];
|
|
base[j] = tmp;
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function isSecretSeed(seed: string): boolean{
|
|
return seed === 'cannonical';
|
|
}
|
|
|
|
|
|
function getRandomQuestion<T>(seed: string, index: number, questions: Question[]): Question {
|
|
if(isSecretSeed(seed)){
|
|
return questions[index] ;
|
|
}
|
|
const numberSeed = hashString(seed);
|
|
const permutations = randomPermutation(numberSeed, questions.length)
|
|
return [... questions[permutations[index]]];
|
|
}
|
|
|
|
function getQuestionPage(question: Question, urls: QuestionURLS, questionIndex: number){
|
|
const source = highlightJs.highlight(question[0].toString(), {language: 'javascript'}).value;
|
|
return (<div>
|
|
<div style="text-align:center">
|
|
{questionIndex + 1}/{ questions.length }
|
|
</div>
|
|
<pre> <code dangerouslySetInnerHTML={{__html: source}}></code></pre>
|
|
<div class="content mt-2 mb-2" >
|
|
{ question[1].question_fr }
|
|
</div>
|
|
|
|
<div id="message">
|
|
</div>
|
|
|
|
<form>
|
|
<div class="field">
|
|
<div class="control is-expanded">
|
|
<input class="input" type="text" placeholder="Answer" name="answer"></input>
|
|
<input type="hidden" name="rendedingTimeStamp" value={Date.now()} ></input>
|
|
</div>
|
|
</div>
|
|
<div class="field is-grouped is-grouped-multiline">
|
|
<div class="control">
|
|
<button class="button is-info"
|
|
hx-trigger="click"
|
|
hx-target="#message"
|
|
hx-post={urls.validationUrl} >
|
|
Verify Answer
|
|
</button>
|
|
</div>
|
|
<div class="control">
|
|
<a class="button is-info"
|
|
href="#"
|
|
hx-trigger="next-question from:body delay:500ms, click"
|
|
hx-get={urls.nextQuestionUrl}
|
|
hx-params="none"
|
|
hx-target="#question-body" >
|
|
Next Question
|
|
</a>
|
|
</div>
|
|
<div class="control">
|
|
<a class="button is-info"
|
|
href="#"
|
|
hx-trigger="click"
|
|
hx-get={urls.answerQuestionUrl}
|
|
hx-params="none"
|
|
hx-target="#message" >
|
|
Show Answer
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>);
|
|
}
|
|
|
|
function getValidAnswerMessage(){
|
|
return (<div class="notification is-primary">
|
|
Fantastic! Good answer!
|
|
</div>)
|
|
}
|
|
|
|
function getInvalidAnswerMessage(){
|
|
return (<div class="notification is-danger">
|
|
Unfortunatly, this is not the right answer.
|
|
</div>)
|
|
}
|
|
|
|
function isValidAnswer(question: Question, answer: string): boolean {
|
|
return question[1].validate(answer);
|
|
}
|
|
|
|
function processInvalidAnswer(ctx, answer: string): void {
|
|
ctx.response.body = renderToString(getInvalidAnswerMessage());
|
|
}
|
|
|
|
function processValidAnswer(ctx, answer: string): void {
|
|
ctx.response.headers.append("HX-Trigger", "next-question");
|
|
ctx.response.body = renderToString(getValidAnswerMessage());
|
|
}
|
|
|
|
function getQuestionIndex(ctx){
|
|
const index = Number.parseInt(ctx.params.idx??"0");
|
|
return index % questions.length;
|
|
}
|
|
|
|
function getNextQuestionIndex(ctx){
|
|
return (getQuestionIndex(ctx) + 1) % questions.length
|
|
}
|
|
|
|
function getQuestion(ctx): Question{
|
|
const seed = ctx.params.seed;
|
|
const index = getQuestionIndex(ctx);
|
|
return getRandomQuestion(seed, index, questions);
|
|
}
|
|
|
|
function getUrls(ctx): QuestionURLS{
|
|
const seed = ctx.params.seed;
|
|
const index = getQuestionIndex(ctx);
|
|
const nextQuestionIndex = getNextQuestionIndex(ctx);
|
|
const nextQuestionUrl = `/api/v1/random-questions/${seed}/${nextQuestionIndex}`;
|
|
const validationUrl = `/api/v1/random-questions/${seed}/${index}/validate`;
|
|
const answerQuestionUrl = `/api/v1/random-questions/${seed}/${index}/answer`
|
|
return {nextQuestionUrl, validationUrl, answerQuestionUrl};
|
|
}
|
|
|
|
async function persistAnswer(ctx, form): Promise<void>{
|
|
const answer = form.get("answer");
|
|
const rendedingTimeStamp = form.get("rendedingTimeStamp");
|
|
const answeredTimestamp = Date.now();
|
|
const kv = await Deno.openKv();
|
|
const q = getQuestion(ctx);
|
|
|
|
const username = ctx.request.headers.get('session');
|
|
const data = {
|
|
ip: ctx.request.ip,
|
|
url: ctx.request.url.toString(),
|
|
date: Date.now(),
|
|
domain: ctx.request.url.hostname,
|
|
hasRightResponse: isValidAnswer(q, answer),
|
|
answer,
|
|
rendedingTimeStamp,
|
|
answeredTimestamp
|
|
}
|
|
const baseKey = ["answer", q[1].id, username];
|
|
for(let i = 0 ; i < 1000; i++){
|
|
const key = baseKey.concat([i]);
|
|
const res = await kv.atomic()
|
|
.check({ key, versionstamp: null }) // `null` versionstamps mean 'no value'
|
|
.set(key, data)
|
|
.commit();
|
|
if (res.ok) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
apiRouter.post("/api/v1/random-questions/:seed/:idx/validate", async (ctx)=> {
|
|
const form = (await ctx.request.body().value);
|
|
const answer = form.get("answer");
|
|
const q = getQuestion(ctx);
|
|
|
|
if(isValidAnswer(q, answer)){
|
|
processValidAnswer(ctx, getUrls(ctx).nextQuestionUrl)
|
|
}else{
|
|
processInvalidAnswer(ctx, getUrls(ctx).nextQuestionUrl)
|
|
}
|
|
await persistAnswer(ctx, form);
|
|
|
|
})
|
|
|
|
apiRouter.get("/api/v1/random-questions/:seed/:idx/answer", (ctx)=> {
|
|
const q = getQuestion(ctx);
|
|
ctx.response.body = renderToString(
|
|
<div class="notification is-warning">
|
|
The answer is {q[1].answer}
|
|
</div>);
|
|
})
|
|
|
|
apiRouter.get("/api/v1/random-questions/:seed/:idx", (ctx)=> {
|
|
const q = getQuestion(ctx);
|
|
const urls = getUrls(ctx);
|
|
const questionIndex = getQuestionIndex(ctx);
|
|
|
|
ctx.response.body = renderToString(getQuestionPage(q, urls, questionIndex));
|
|
ctx.response.headers.append("HX-Trigger", JSON.stringify({'set-question-id': ctx.params.idx}));
|
|
});
|
|
|
|
|
|
loginRouter.get("/toto", (ctx)=> {
|
|
ctx.response.body = highlightJs.highlight('function(){console.log("hello world")}', {language: 'javascript'}).value;
|
|
});
|
|
|
|
|
|
loginRouter.post("/api/v1/login", async (ctx)=> {
|
|
const username = (await ctx.request.body().value).get("username");
|
|
ctx.response.headers.append("HX-Trigger", JSON.stringify({'set-seed': username}));
|
|
ctx.response.body = username;
|
|
});
|
|
|
|
loginRouter.post("/api/v1/logout", (ctx)=> {
|
|
ctx.response.headers.append("HX-Trigger", "logout");
|
|
ctx.response.body = "";
|
|
});
|
|
|
|
|
|
app.use(loginRouter.routes());
|
|
app.use(staticFiles("public", {brotli: true, fetch: true}));
|
|
app.use(async (ctx, next) => {
|
|
if(!ctx.request.headers.get('session')){
|
|
console.log("user not logged in.")
|
|
ctx.response.headers.append("HX-Redirect", "/login.html");
|
|
return
|
|
}
|
|
await next();
|
|
})
|
|
app.use(apiRouter.routes());
|
|
|
|
|
|
|
|
await app.listen({ port: 8000 });
|