
When a level is finished, the vocabulary of that item will be converted into SM2 metadata, which will be appended to the user's vocabMetadata.
398 lines
10 KiB
TypeScript
398 lines
10 KiB
TypeScript
import * as express from "express";
|
|
import * as bodyparser from "body-parser";
|
|
|
|
import { Db } from "mongodb";
|
|
|
|
import { authRoute } from "../security/token";
|
|
|
|
import { userFromSession } from "../utils/user";
|
|
|
|
import { IUser, IUserDBModel } from "../models/user";
|
|
import { ISM2Metadata } from "../models/review";
|
|
import { LRequest } from "../types/express";
|
|
|
|
const userRouter = express.Router();
|
|
userRouter.use(authRoute);
|
|
userRouter.use(bodyparser.json());
|
|
|
|
// Return the user object if the user is still authenticated
|
|
userRouter.get("/me", async (req: LRequest, res) => {
|
|
const { db, token } = req;
|
|
|
|
const session = await db.collection("sessions").findOne({ token, });
|
|
if (session !== null) {
|
|
const user = await db.collection("users").findOne({ username: session.username });
|
|
|
|
// Copy and strip unneeded attributes
|
|
let copy = Object.assign({}, user, {
|
|
sessionToken: token,
|
|
});
|
|
delete copy._id;
|
|
delete copy.hash;
|
|
delete copy.salt;
|
|
|
|
res.send({
|
|
error: "0",
|
|
data: copy,
|
|
});
|
|
} else {
|
|
res.send({
|
|
error: "404",
|
|
data: {},
|
|
});
|
|
}
|
|
});
|
|
|
|
// Removes the user's session
|
|
userRouter.get("/logout", async (req: LRequest, res) => {
|
|
// Try to remove the session
|
|
const { db, token } = req;
|
|
await db.collection("sessions").findOneAndDelete({ token, });
|
|
|
|
res.send({
|
|
error: "0",
|
|
data: {},
|
|
});
|
|
});
|
|
|
|
// TODO: This should be shared with the frontend, to remove code duplication
|
|
export enum VocabType {
|
|
NOMEN = 0,
|
|
VERB = 1,
|
|
ADJEKTIV = 2,
|
|
ADVERB = 3,
|
|
};
|
|
|
|
// Gets the user's review queue
|
|
userRouter.get("/queue", async (req: LRequest, res) => {
|
|
// TODO: if (user)
|
|
const { token, db } = req;
|
|
let user = Object.assign({}, await userFromSession(token, db));
|
|
|
|
const sm2 = user.vocabMetadata;
|
|
const data = Object.keys(sm2).map(id => Object.assign({}, sm2[parseInt(id)], { id, }));
|
|
const sorted = data.sort((a: any, b: any) => {
|
|
if (a.nextDueDate > b.nextDueDate)
|
|
return 1;
|
|
if (a.nextDueDate < b.nextDueDate)
|
|
return -1;
|
|
|
|
return 0;
|
|
}).slice(0, 20)
|
|
.map((el: any) => {
|
|
return parseInt(el.id);
|
|
});
|
|
|
|
// Fetch all vocab ids from the vocabulary collection
|
|
const vocabRaw = await db.collection("vocabulary").find({
|
|
id: { $in: sorted },
|
|
}, {
|
|
// TODO: Make this configurable?
|
|
sort: {
|
|
|
|
},
|
|
limit: 20,
|
|
}).toArray();
|
|
|
|
// Remove unneeded data
|
|
const vocab = vocabRaw.map(el => {
|
|
let tmp = Object.assign({}, el);
|
|
delete tmp._id;
|
|
|
|
return tmp;
|
|
});
|
|
|
|
res.send({
|
|
error: "0",
|
|
data: {
|
|
queue: vocab,
|
|
},
|
|
});
|
|
});
|
|
|
|
// Get ot set the last review results
|
|
userRouter.get("/lastReview", async (req: LRequest, res) => {
|
|
// TODO: if(user)
|
|
const { token, db } = req;
|
|
const user = await userFromSession(token, db);
|
|
|
|
res.send({
|
|
error: "0",
|
|
data: user.lastReview,
|
|
});
|
|
});
|
|
|
|
function dateInNDays(n: number): number {
|
|
let today = new Date();
|
|
today.setDate(today.getDate() + n);
|
|
return Date.parse(today.toString());
|
|
}
|
|
|
|
userRouter.post("/lastReview", async (req: LRequest, res) => {
|
|
// Check if we get the needed data
|
|
if (!req.body || !("meta" in req.body) || !("sm2" in req.body)) {
|
|
res.send({
|
|
error: "400",
|
|
data: {
|
|
msg: "Last review state or SM2 data not specified!",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const { token, db } = req;
|
|
let user = Object.assign({}, await userFromSession(token, db));
|
|
if (!user) {
|
|
res.send({
|
|
error: "404",
|
|
data: {
|
|
msg: "Failed to find user with the specified session!",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
Object.keys(req.body.sm2).forEach((id: string) => {
|
|
const vocabId = parseInt(id);
|
|
const correct: boolean = req.body.sm2[id];
|
|
let vocab_sm2: ISM2Metadata = Object.assign({},
|
|
user.vocabMetadata[vocabId]);
|
|
|
|
// TODO: Tuning?
|
|
// TODO: Move into another module
|
|
const perf = correct ? 3 : 1;
|
|
vocab_sm2.easiness += -0.8 + 0.28 * perf + 0.02 * Math.pow(perf, 2);
|
|
// Update the consecutive correct answers and the due date
|
|
if (correct) {
|
|
vocab_sm2.consecutiveCorrectAnswers += 1;
|
|
vocab_sm2.nextDueDate = dateInNDays(6 * Math.pow(vocab_sm2.easiness, vocab_sm2.consecutiveCorrectAnswers - 1));
|
|
} else {
|
|
vocab_sm2.consecutiveCorrectAnswers = 0;
|
|
vocab_sm2.nextDueDate = dateInNDays(1);
|
|
}
|
|
|
|
user.vocabMetadata[vocabId] = vocab_sm2;
|
|
});
|
|
|
|
// Update the last review
|
|
user.lastReview = req.body.meta;
|
|
|
|
// TODO: Error handling
|
|
await db.collection("users").updateOne({
|
|
username: user.username,
|
|
}, {
|
|
$set: {
|
|
lastReview: user.lastReview,
|
|
vocabMetadata: user.vocabMetadata,
|
|
},
|
|
});
|
|
|
|
res.send({
|
|
error: "0",
|
|
data: {},
|
|
});
|
|
});
|
|
|
|
// Returns the next level (level + 1) or the current level, if no higher level
|
|
// can be found
|
|
async function getNextLevel(token: string, db: Db): Promise<any> {
|
|
// TODO: if(user)
|
|
const user = await userFromSession(token, db);
|
|
const { lastLevel } = user;
|
|
|
|
// Try to find a level, which level is lastLevel + 1
|
|
const level = await db.collection("levels").findOne({
|
|
level: lastLevel + 1,
|
|
});
|
|
|
|
if (level) {
|
|
return level;
|
|
} else {
|
|
// TODO: Send different data, so that the Client can say "Hey, no more levels"
|
|
return await db.collection("levels").findOne({
|
|
level: lastLevel
|
|
});
|
|
}
|
|
}
|
|
|
|
// Get the next level
|
|
userRouter.get("/nextLevel", async (req: LRequest, res) => {
|
|
const level = await getNextLevel(req.token, req.db);
|
|
res.send({
|
|
error: "0",
|
|
data: level,
|
|
});
|
|
});
|
|
|
|
// Mark a level as done
|
|
userRouter.post("/level/:id", async (req: LRequest, res) => {
|
|
// Is everything specified?
|
|
if (!req.params || !req.params.id) {
|
|
res.send({
|
|
error: "400",
|
|
data: {
|
|
msg: "No level ID specified",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const id = parseInt(req.params.id);
|
|
if (id === NaN) {
|
|
res.send({
|
|
error: "400",
|
|
data: {
|
|
msg: "Invalid level ID",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// TODO: if (user)
|
|
const { token, db } = req;
|
|
const user = await userFromSession(token, db);
|
|
|
|
if (id in user.levels) {
|
|
// Nothing
|
|
} else {
|
|
// The level is new to the user
|
|
// Is the new level higher than the "highest" already completed level?
|
|
let update = {
|
|
levels: user.levels.concat(id),
|
|
};
|
|
if (id > Math.max(...user.levels)) {
|
|
// TODO: if (!level)
|
|
const level = await db.collection("levels").findOne({
|
|
level: id,
|
|
});
|
|
|
|
// Convert the level's vocabulary to SM2 metadata
|
|
let sm2: { [id: number]: ISM2Metadata } = {};
|
|
level.vocab.forEach((id: number) => {
|
|
sm2[id] = {
|
|
easiness: 1.3,
|
|
consecutiveCorrectAnswers: 0,
|
|
nextDueDate: Date.parse((new Date()).toString()),
|
|
};
|
|
});
|
|
const newVocabMetadata = Object.assign({}, user.vocabMetadata, sm2);
|
|
|
|
// Also update the lastLevel attribute
|
|
Object.assign(update, {
|
|
lastLevel: id,
|
|
vocabMetadata: newVocabMetadata,
|
|
});
|
|
}
|
|
await db.collection("users").updateOne({
|
|
username: user.username,
|
|
}, {
|
|
$set: update,
|
|
});
|
|
}
|
|
|
|
res.send({
|
|
error: "200",
|
|
data: {},
|
|
});
|
|
});
|
|
|
|
// Get the data needed for the dashboard
|
|
userRouter.get("/dashboard", async (req: LRequest, res) => {
|
|
const { db, token } = req;
|
|
|
|
// Get the user
|
|
// TODO: if (user)
|
|
const user = await userFromSession(token, db);
|
|
const { classId } = user;
|
|
|
|
// Fetch the top ten of the class
|
|
const rawTopTen = await db.collection("users").find({
|
|
classId,
|
|
}, {
|
|
sort: {
|
|
score: -1,
|
|
},
|
|
limit: 10,
|
|
}).toArray();
|
|
|
|
let nr = 1;
|
|
const topTen = rawTopTen.map((user: IUser) => {
|
|
return {
|
|
username: user.username,
|
|
score: user.score,
|
|
// TODO: Calculate on the client?
|
|
level: 1,
|
|
nr: nr++,
|
|
};
|
|
});
|
|
|
|
const nextLevel = await getNextLevel(token, db);
|
|
res.send({
|
|
error: "200",
|
|
data: {
|
|
nextLevel,
|
|
topTen,
|
|
lastReview: user.lastReview,
|
|
},
|
|
});
|
|
});
|
|
|
|
userRouter.post("/showWelcome", async (req: LRequest, res) => {
|
|
const { db, token } = req;
|
|
|
|
// Are all arguments specified?
|
|
if (!req.body || !("state" in req.body)) {
|
|
res.send({
|
|
error: "400",
|
|
data: {
|
|
msg: "State not specified",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Find the user
|
|
// TODO: if (user)
|
|
const user = await userFromSession(token, db);
|
|
|
|
await db.collection("users").updateOne({
|
|
username: user.username,
|
|
}, {
|
|
$set: {
|
|
showWelcome: req.body.state,
|
|
},
|
|
});
|
|
res.send({
|
|
error: "200",
|
|
data: {},
|
|
});
|
|
});
|
|
|
|
userRouter.post("/score", async (req: LRequest, res) => {
|
|
const { token, db } = req;
|
|
|
|
// Are all arguments specified
|
|
if (!req.body || !("score" in req.body)) {
|
|
res.send({
|
|
error: "400",
|
|
data: {
|
|
msg: "No score specified!",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// TODO: if (user)
|
|
const user = await userFromSession(token, db);
|
|
// Update the score
|
|
db.collection("users").updateOne({
|
|
username: user.username,
|
|
}, {
|
|
$set: {
|
|
score: req.body.score,
|
|
},
|
|
});
|
|
});
|
|
|
|
export default userRouter;
|