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(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 (
{questionIndex + 1}/{ questions.length }
 
{ question[1].question_fr }
); } function getValidAnswerMessage(){ return (
Fantastic! Good answer!
) } function getInvalidAnswerMessage(){ return (
Unfortunatly, this is not the right answer.
) } 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{ 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(
The answer is {q[1].answer}
); }) 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 });