GitLab steht wegen Wartungsarbeiten am Montag, den 10. Mai, zwischen 17:00 und 19:00 Uhr nicht zur Verfügung.

QuizRouter.ts 28.2 KB
Newer Older
1
import { Response } from 'express';
Fullarton's avatar
Fullarton committed
2
import * as fs from 'fs';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
3
import { DeleteWriteOpResultObject } from 'mongodb';
Fullarton's avatar
Fullarton committed
4
import * as path from 'path';
5
import {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  BadRequestError,
  BodyParam,
  ContentType,
  Delete,
  Get,
  HeaderParam,
  InternalServerError,
  JsonController,
  NotFoundError,
  Param,
  Params,
  Post,
  Put,
  Res,
  UnauthorizedError,
  UploadedFiles,
22
} from 'routing-controllers';
23
import AMQPConnector from '../../db/AMQPConnector';
24
import { default as DbDAO } from '../../db/DbDAO';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
25 26
import MemberDAO from '../../db/MemberDAO';
import QuizDAO from '../../db/quiz/QuizDAO';
27
import UserDAO from '../../db/UserDAO';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
28 29 30 31 32 33
import { AbstractAnswerEntity } from '../../entities/answer/AbstractAnswerEntity';
import { QuizEntity } from '../../entities/quiz/QuizEntity';
import { DbCollection } from '../../enums/DbOperation';
import { MessageProtocol, StatusProtocol } from '../../enums/Message';
import { QuizState } from '../../enums/QuizState';
import { QuizVisibility } from '../../enums/QuizVisibility';
34
import { UserRole } from '../../enums/UserRole';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
35
import { ExcelWorkbook } from '../../export/ExcelWorkbook';
36
import { IMessage } from '../../interfaces/communication/IMessage';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
37 38
import { IQuizStatusPayload } from '../../interfaces/IQuizStatusPayload';
import { IQuizEntity, IQuizSerialized } from '../../interfaces/quizzes/IQuizEntity';
39
import { asyncForEach } from '../../lib/async-for-each';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
40 41 42
import { MatchTextToAssetsDb } from '../../lib/cache/assets';
import { Leaderboard } from '../../lib/leaderboard/leaderboard';
import { QuizModel } from '../../models/quiz/QuizModelItem';
43 44 45 46 47
import { settings, staticStatistics } from '../../statistics';
import { AbstractRouter } from './AbstractRouter';

@JsonController('/api/v1/quiz')
export class QuizRouter extends AbstractRouter {
48
  private readonly _leaderboard: Leaderboard = new Leaderboard();
Fullarton's avatar
Fullarton committed
49

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
50 51 52 53
  @Get('/status/:quizName?')
  public getIsAvailableQuiz(
    @Params() params: { [key: string]: any }, //
    @HeaderParam('authorization', { required: false }) token: string, //
54
  ): IMessage {
Fullarton's avatar
Fullarton committed
55

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
56 57
    const quizName = params.quizName;
    const member = MemberDAO.getMemberByToken(token);
Fullarton's avatar
Fullarton committed
58

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
59 60 61 62 63 64
    if (!quizName && (!token || !member)) {
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
    }

    const quiz: IQuizEntity = QuizDAO.getQuizByName(quizName || member.currentQuizName);
    const payload: IQuizStatusPayload = {};
Fullarton's avatar
Fullarton committed
65 66

    if (quiz) {
67
      if ([QuizState.Active, QuizState.Running].includes(quiz.state)) {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
68 69 70 71
        payload.provideNickSelection = quiz.sessionConfig.nicks.selectedNicks.length > 0;
        payload.authorizeViaCas = quiz.sessionConfig.nicks.restrictToCasLogin;
        payload.maxMembersPerGroup = quiz.sessionConfig.nicks.maxMembersPerGroup;
        payload.autoJoinToGroup = quiz.sessionConfig.nicks.autoJoinToGroup;
72
        payload.memberGroups = quiz.sessionConfig.nicks.memberGroups;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
73
        payload.startTimestamp = quiz.currentStartTimestamp;
74
        payload.readingConfirmationRequested = quiz.readingConfirmationRequested;
75
      }
Fullarton's avatar
Fullarton committed
76

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
77 78 79
      payload.name = quiz.name;
      payload.state = quiz.state;
    }
80

81
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
82
      status: StatusProtocol.Success,
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
83 84 85 86 87 88 89
      step: quiz ? //
            [QuizState.Active, QuizState.Running].includes(quiz.state) ? //
            MessageProtocol.Available : //
            quiz.privateKey === token ? //
            MessageProtocol.Editable : //
            MessageProtocol.AlreadyTaken : //
            MessageProtocol.Unavailable, //
90
      payload,
Fullarton's avatar
Fullarton committed
91 92 93
    };
  }

94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
  @Get('/full-status/:quizName?')
  public getFullQuizStatusData(
    @Params() params: { [key: string]: any }, //
    @HeaderParam('authorization', { required: false }) token: string, //
  ): object {
    const status = this.getIsAvailableQuiz(params, token);
    const quiz = this.getQuiz(params, token);
    return {
      status: status.status === StatusProtocol.Success && quiz.status === StatusProtocol.Success ? StatusProtocol.Success : StatusProtocol.Failed,
      step: status.step === MessageProtocol.Available && quiz.step === MessageProtocol.Available ? MessageProtocol.Available
                                                                                                 : MessageProtocol.Unavailable,
      payload: {
        status: status.payload,
        quiz: quiz.payload,
      },
    };
  }

112 113 114 115 116 117
  @Get('/generate/demo/:languageId')
  public generateDemoQuiz(
    @Param('languageId') languageId: string, //
    @Res() res: Response, //
  ): object {

Fullarton's avatar
Fullarton committed
118
    try {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
119
      const basePath = path.join(staticStatistics.pathToAssets, 'predefined_quizzes', 'demo_quiz');
120
      let demoQuizPath = path.join(basePath, `${languageId.toLowerCase()}.demo_quiz.json`);
Fullarton's avatar
Fullarton committed
121 122 123
      if (!fs.existsSync(demoQuizPath)) {
        demoQuizPath = path.join(basePath, 'en.demo_quiz.json');
      }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
124 125 126
      const result: IQuizEntity = JSON.parse(fs.readFileSync(demoQuizPath).toString());
      result.name = 'Demo Quiz ' + (QuizDAO.getLastPersistedDemoQuizNumber() + 1);
      QuizDAO.convertLegacyQuiz(result);
Fullarton's avatar
Fullarton committed
127
      res.setHeader('Response-Type', 'application/json');
128
      return result;
Fullarton's avatar
Fullarton committed
129
    } catch (ex) {
130
      throw new InternalServerError(`File IO Error: ${ex}`);
Fullarton's avatar
Fullarton committed
131 132 133
    }
  }

134 135 136 137 138 139 140
  @Get('/generate/abcd/:languageId/:answerLength?')
  public generateAbcdQuiz(
    @Param('languageId') languageId: string, //
    @Param('answerLength') answerLength: number, //
    @Res() res: Response, //
  ): object {

Fullarton's avatar
Fullarton committed
141
    try {
142
      answerLength = answerLength || 4;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
143
      const basePath = path.join(staticStatistics.pathToAssets, 'predefined_quizzes', 'abcd_quiz');
144
      let abcdQuizPath = path.join(basePath, `${languageId.toLowerCase()}.abcd_quiz.json`);
Fullarton's avatar
Fullarton committed
145 146 147
      if (!fs.existsSync(abcdQuizPath)) {
        abcdQuizPath = path.join(basePath, 'en.abcd_quiz.json');
      }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
148
      const result: IQuizSerialized = JSON.parse(fs.readFileSync(abcdQuizPath).toString());
Fullarton's avatar
Fullarton committed
149
      let abcdName = '';
150
      for (let i = 0; i < answerLength; i++) {
Fullarton's avatar
Fullarton committed
151 152
        abcdName += String.fromCharCode(65 + i);
      }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
153 154
      result.name = `${abcdName} ${(QuizDAO.getLastPersistedAbcdQuizNumberByLength(answerLength) + 1)}`;
      QuizDAO.convertLegacyQuiz(result);
Fullarton's avatar
Fullarton committed
155
      res.setHeader('Response-Type', 'application/json');
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
156
      return new QuizEntity(result).serialize();
Fullarton's avatar
Fullarton committed
157
    } catch (ex) {
158
      throw new InternalServerError(`File IO Error: ${ex}`);
Fullarton's avatar
Fullarton committed
159 160 161
    }
  }

162
  @Post('/upload')
163
  public async uploadQuiz(
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
164 165
    @HeaderParam('authorization') privateKey: string, //
    @UploadedFiles('uploadFiles[]') uploadedFiles: any, //
166
  ): Promise<object> {
167

Fullarton's avatar
Fullarton committed
168 169
    const duplicateQuizzes = [];
    const quizData = [];
170

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
171 172 173
    uploadedFiles.forEach(file => {
      quizData.push({
        fileName: file.originalname,
174
        quiz: QuizDAO.convertLegacyQuiz(JSON.parse(file.buffer.toString('UTF-8'))),
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
175 176
      });
    });
177

178
    await asyncForEach(quizData, async (data: { fileName: string, quiz: IQuizEntity }) => {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
179 180 181 182 183 184
      const existingQuiz = QuizDAO.getQuizByName(data.quiz.name);
      if (existingQuiz) {
        duplicateQuizzes.push({
          quizName: data.quiz.name,
          fileName: data.fileName,
          renameRecommendation: QuizDAO.getRenameRecommendations(data.quiz.name),
185
        });
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
186 187 188 189 190 191 192 193 194 195 196
      } else {
        data.quiz.privateKey = privateKey;
        data.quiz.visibility = QuizVisibility.Account;

        const quizValidator = new QuizModel(data.quiz);
        const result = quizValidator.validateSync();

        if (result) {
          throw result;
        }

197
        await quizValidator.save();
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
198 199 200 201 202 203
      }
    });

    return {
      status: StatusProtocol.Success,
      step: MessageProtocol.UploadFile,
204 205 206 207
      payload: {
        duplicateQuizzes,
        quizData: quizData.filter(insertedQuiz => !duplicateQuizzes.find(duplicateQuiz => duplicateQuiz.fileName === insertedQuiz.fileName)),
      },
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
208
    };
Fullarton's avatar
Fullarton committed
209 210
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
211
  @Post('/next')
212 213 214 215 216
  public async startQuiz(
    @HeaderParam('authorization') token: string, //
    @BodyParam('quizName') quizName: string, //
  ): Promise<object> {
    const quiz = QuizDAO.getQuizByName(quizName);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
217
    if (!quiz || ![QuizState.Active, QuizState.Running].includes(quiz.state)) {
218 219 220 221 222
      return {
        status: StatusProtocol.Failed,
        step: MessageProtocol.IsInactive,
        payload: {},
      };
Fullarton's avatar
Fullarton committed
223 224
    }

225 226 227 228
    if (quiz.privateKey !== token) {
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
    }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
229 230
    if (quiz.sessionConfig.readingConfirmationEnabled && !quiz.readingConfirmationRequested) {
      const nextQuestionIndex = quiz.nextQuestion();
Fullarton's avatar
Fullarton committed
231
      if (nextQuestionIndex === -1) {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
232 233 234 235
        throw new BadRequestError(MessageProtocol.EndOfQuestions);
      }

      quiz.requestReadingConfirmation();
236 237 238 239 240 241 242 243 244 245

      try {
        await DbDAO.updateOne(DbCollection.Quizzes, { _id: QuizDAO.getQuizByName(quiz.name).id }, {
          readingConfirmationRequested: true,
          state: QuizState.Running,
        });
      } catch (e) {
        throw new InternalServerError(e);
      }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
246 247 248 249 250 251
      return {
        status: StatusProtocol.Success,
        step: MessageProtocol.ReadingConfirmationRequested,
      };
    } else if (quiz.readingConfirmationRequested) {
      const currentStartTimestamp: number = new Date().getTime();
252 253 254 255 256 257 258 259 260 261

      try {
        await DbDAO.updateOne(DbCollection.Quizzes, { _id: QuizDAO.getQuizByName(quiz.name).id }, {
          currentStartTimestamp,
          readingConfirmationRequested: false,
          state: QuizState.Running,
        });
      } catch (e) {
        throw new InternalServerError(e);
      }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
262 263

      quiz.readingConfirmationRequested = false;
264
      quiz.startNextQuestion();
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
265 266 267 268 269 270 271 272
      return {
        status: StatusProtocol.Success,
        step: MessageProtocol.Start,
        payload: {
          currentStartTimestamp,
          currentQuestionIndex: quiz.currentQuestionIndex,
        },
      };
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
273 274 275 276
    } else {
      const nextQuestionIndex = quiz.nextQuestion();
      if (nextQuestionIndex === -1) {
        throw new BadRequestError(MessageProtocol.EndOfQuestions);
Fullarton's avatar
Fullarton committed
277
      }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
278
      const currentStartTimestamp: number = new Date().getTime();
279 280 281 282 283 284 285 286 287 288

      try {
        await DbDAO.updateOne(DbCollection.Quizzes, { _id: QuizDAO.getQuizByName(quiz.name).id }, {
          currentStartTimestamp,
          readingConfirmationRequested: false,
          state: QuizState.Running,
        });
      } catch (e) {
        throw new InternalServerError(e);
      }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
289 290

      quiz.readingConfirmationRequested = false;
291
      quiz.currentStartTimestamp = currentStartTimestamp;
292
      quiz.startNextQuestion();
293

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
294 295 296 297 298 299 300 301
      return {
        status: StatusProtocol.Success,
        step: MessageProtocol.Start,
        payload: {
          currentStartTimestamp,
          nextQuestionIndex,
        },
      };
Fullarton's avatar
Fullarton committed
302 303 304
    }
  }

305 306 307 308
  @Post('/stop')
  public stopQuiz(@BodyParam('quizName') quizName: string, //
  ): object {

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
309
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
Fullarton's avatar
Fullarton committed
310
    if (!activeQuiz) {
311
      return;
Fullarton's avatar
Fullarton committed
312
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
313

314
    DbDAO.updateOne(DbCollection.Quizzes, { _id: QuizDAO.getQuizByName(quizName).id }, { currentStartTimestamp: -1 });
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
315

316 317
    activeQuiz.stop();

318
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
319 320
      status: StatusProtocol.Success,
      step: MessageProtocol.Stop,
321
      payload: {},
322
    };
Fullarton's avatar
Fullarton committed
323 324
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
325 326 327 328
  @Get('/start-time')
  public getStartTime(@HeaderParam('authorization') token: string): number {
    const member = MemberDAO.getMemberByToken(token);
    if (!member) {
329
      console.error('Unknown member');
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
330 331 332 333 334
      throw new BadRequestError('Unknown member');
    }

    const quiz = QuizDAO.getQuizByName(member.currentQuizName);
    if (!quiz || ![QuizState.Active, QuizState.Running].includes(quiz.state)) {
335
      console.error('Quiz is not active and not running');
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
336 337 338 339 340 341
      throw new BadRequestError('Quiz is not active and not running');
    }

    return quiz.currentStartTimestamp;
  }

342 343 344 345
  @Get('/currentState/:quizName')
  public getCurrentQuizState(@Param('quizName') quizName: string, //
  ): object {

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
346
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
Fullarton's avatar
Fullarton committed
347
    if (!activeQuiz) {
348
      return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
349 350
        status: StatusProtocol.Failed,
        step: MessageProtocol.IsInactive,
351
        payload: {},
352
      };
Fullarton's avatar
Fullarton committed
353 354
    }
    const index = activeQuiz.currentQuestionIndex < 0 ? 0 : activeQuiz.currentQuestionIndex;
355
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
356 357
      status: StatusProtocol.Success,
      step: MessageProtocol.CurrentState,
358
      payload: {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
359
        questions: activeQuiz.questionList.slice(0, index + 1).map(question => question.serialize()),
Fullarton's avatar
Fullarton committed
360 361
        questionIndex: index,
        startTimestamp: activeQuiz.currentStartTimestamp,
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
362
        numberOfQuestions: activeQuiz.questionList.length,
363
      },
364
    };
Fullarton's avatar
Fullarton committed
365 366
  }

367 368 369 370
  @Post('/reading-confirmation')
  public showReadingConfirmation(@BodyParam('quizName') quizName: string, //
  ): object {

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
371
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
Fullarton's avatar
Fullarton committed
372
    if (!activeQuiz) {
373
      return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
374
        status: StatusProtocol.Failed,
375
        step: MessageProtocol.ReadingConfirmationRequested,
376
        payload: {},
377
      };
Fullarton's avatar
Fullarton committed
378 379 380
    }
    activeQuiz.nextQuestion();
    activeQuiz.requestReadingConfirmation();
381
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
382 383
      status: StatusProtocol.Success,
      step: MessageProtocol.ReadingConfirmationRequested,
384
      payload: {},
385
    };
Fullarton's avatar
Fullarton committed
386 387
  }

388 389 390 391
  @Get('/startTime/:quizName')
  public getQuizStartTime(@Param('quizName') quizName: string, //
  ): object {

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
392
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
Fullarton's avatar
Fullarton committed
393
    if (!activeQuiz) {
394
      return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
395
        status: StatusProtocol.Failed,
396
        step: MessageProtocol.GetStartTime,
397
        payload: {},
398
      };
Fullarton's avatar
Fullarton committed
399
    }
400
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
401 402
      status: StatusProtocol.Success,
      step: MessageProtocol.GetStartTime,
403
      payload: { startTimestamp: activeQuiz.currentStartTimestamp },
404
    };
Fullarton's avatar
Fullarton committed
405 406
  }

407 408 409 410
  @Get('/settings/:quizName')
  public getQuizSettings(@Param('quizName') quizName: string, //
  ): object {

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
411
    const activeQuiz: IQuizEntity = QuizDAO.getQuizByName(quizName);
Fullarton's avatar
Fullarton committed
412
    if (!activeQuiz) {
413
      return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
414
        status: StatusProtocol.Failed,
415
        step: MessageProtocol.UpdatedSettings,
416
        payload: {},
417
      };
Fullarton's avatar
Fullarton committed
418
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
419

420
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
421 422 423
      status: StatusProtocol.Success,
      step: MessageProtocol.UpdatedSettings,
      payload: { settings: activeQuiz.sessionConfig.serialize() },
424
    };
Fullarton's avatar
Fullarton committed
425 426
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
427
  @Post('/settings')
428
  public updateQuizSettings(
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
429
    @HeaderParam('authorization') token: string, //
430
    @BodyParam('quizName') quizName: string, //
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
431
    @BodyParam('settings') quizSettings: { state: boolean, target: string }, //
432 433
  ): object {

434
    const activeQuiz: IQuizEntity = QuizDAO.getQuizByName(quizName);
Fullarton's avatar
Fullarton committed
435
    if (!activeQuiz) {
436 437 438 439 440
      return {
        status: StatusProtocol.Failed,
        step: MessageProtocol.UpdatedSettings,
        payload: {},
      };
441 442
    }
    if (activeQuiz.privateKey !== token) {
443
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
Fullarton's avatar
Fullarton committed
444
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
445

446
    DbDAO.updateOne(DbCollection.Quizzes, { _id: activeQuiz.id }, { ['sessionConfig.' + quizSettings.target]: quizSettings.state });
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
447

448
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
449 450
      status: StatusProtocol.Success,
      step: MessageProtocol.UpdatedSettings,
451
      payload: {},
452
    };
Fullarton's avatar
Fullarton committed
453 454
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
455 456 457 458 459 460 461 462
  @Put('/')
  public async addQuiz(
    @HeaderParam('authorization') privateKey: string, //
    @BodyParam('quiz') quiz: IQuizSerialized, //
    @BodyParam('serverPassword', { required: settings.public.createQuizPasswordRequired }) serverPassword: string, //
  ): Promise<IQuizSerialized> {
    if (!quiz) {
      throw new BadRequestError(MessageProtocol.InvalidParameters);
Fullarton's avatar
Fullarton committed
463
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
464
    const activeQuizzesAmount = QuizDAO.getActiveQuizzes();
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
465
    if (activeQuizzesAmount.length >= settings.public.limitActiveQuizzes) {
466
      throw new BadRequestError(MessageProtocol.TooMuchActiveQuizzes);
Fullarton's avatar
Fullarton committed
467
    }
468
    if (settings.public.createQuizPasswordRequired) {
469
      if (!serverPassword) {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
470
        throw new UnauthorizedError(MessageProtocol.ServerPasswordRequired);
471
      }
472
      if (serverPassword !== settings.createQuizPassword) {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
473
        throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
474
      }
Fullarton's avatar
Fullarton committed
475 476
    }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
477
    if (settings.public.cacheQuizAssets) {
478

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
479 480 481 482 483 484 485 486 487 488
      const promises: Array<Promise<any>> = [];

      quiz.questionList.forEach(question => {
        promises.push(MatchTextToAssetsDb(question.questionText).then(val => question.questionText = val));
        question.answerOptionList.forEach((answerOption: AbstractAnswerEntity) => {
          promises.push(MatchTextToAssetsDb(answerOption.answerText).then(val => answerOption.answerText = val));
        });
      });

      await Promise.all<any>(promises);
489
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
490

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
491 492
    quiz.currentQuestionIndex = -1;
    quiz.currentStartTimestamp = -1;
493
    quiz.readingConfirmationRequested = false;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
494
    quiz.privateKey = privateKey;
495
    quiz.state = quiz.questionList.length > 0 ? QuizState.Active : QuizState.Inactive;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
496

497
    QuizDAO.convertLegacyQuiz(quiz);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
498 499 500 501 502 503 504
    const quizValidator = new QuizModel(quiz);
    const result = quizValidator.validateSync();

    if (result) {
      throw result;
    }

505 506 507 508 509 510 511 512
    AMQPConnector.channel.publish('global', '.*', Buffer.from(JSON.stringify({
      status: StatusProtocol.Success,
      step: quiz.state === QuizState.Active ? MessageProtocol.SetActive : MessageProtocol.SetInactive,
      payload: {
        quizName: quiz.name,
      },
    })));

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
513 514 515 516 517 518
    const existingQuiz = QuizDAO.getQuizByName(quiz.name);
    if (existingQuiz) {
      if (existingQuiz.privateKey !== privateKey) {
        throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
      }
      const newQuiz = Object.assign({}, existingQuiz.serialize(), quiz);
519
      await DbDAO.updateOne(DbCollection.Quizzes, { _id: existingQuiz.id }, newQuiz);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
520 521 522 523 524 525
      return new QuizEntity(newQuiz).serialize();

    } else {
      const doc = await quizValidator.save();
      return new QuizEntity(doc).serialize();
    }
526 527
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
528 529 530 531 532 533 534 535 536 537
  @Put('/save')
  public saveQuiz(
    @HeaderParam('authorization') privateKey: string, //
    @BodyParam('quiz') quiz: IQuizSerialized, //
  ): void {
    const existingQuiz = QuizDAO.getQuizByName(quiz.name);
    if (existingQuiz) {
      if (existingQuiz.privateKey !== privateKey) {
        throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
      }
538
      QuizDAO.convertLegacyQuiz(quiz);
539

540
      DbDAO.updateOne(DbCollection.Quizzes, { _id: existingQuiz.id }, quiz);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
541
      return;
Fullarton's avatar
Fullarton committed
542
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
543 544

    quiz.privateKey = privateKey;
545
    quiz.expiry = quiz.expiry ? new Date(quiz.expiry) : quiz.expiry;
546
    quiz.state = QuizState.Inactive;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
547

548
    QuizDAO.convertLegacyQuiz(quiz);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569
    const quizValidator = new QuizModel(quiz);
    const result = quizValidator.validateSync();

    if (result) {
      throw result;
    }

    quizValidator.save();
  }

  @Delete('/:quizName')
  public async deleteQuiz(
    @Param('quizName') quizName: string, //
    @HeaderParam('authorization') privateKey: string, //
  ): Promise<object> {
    const quiz = QuizDAO.getQuizByName(quizName);
    if (!quiz || quiz.privateKey !== privateKey) {
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
    }
    const dbResult: DeleteWriteOpResultObject = await DbDAO.deleteOne(DbCollection.Quizzes, {
      name: quizName,
570
      privateKey: privateKey,
571
    });
572
    if (dbResult && dbResult.result.ok) {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
573
      quiz.onRemove();
574
      return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
575 576
        status: StatusProtocol.Success,
        step: MessageProtocol.Removed,
577
        payload: {},
578
      };
Fullarton's avatar
Fullarton committed
579
    } else {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
580
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
Fullarton's avatar
Fullarton committed
581 582 583
    }
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
584
  @Delete('/active/:quizName')
585
  public deleteActiveQuiz(
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
586 587
    @Param('quizName') quizName: string, //
    @HeaderParam('authorization') privateKey: string, //
588 589 590 591
  ): object {

    if (!quizName || !privateKey) {
      throw new BadRequestError(JSON.stringify({
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
592 593
        status: StatusProtocol.Failed,
        step: MessageProtocol.InvalidParameters,
594
        payload: {},
595
      }));
Fullarton's avatar
Fullarton committed
596
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
597

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
598 599 600
    const quiz = QuizDAO.getQuizByName(quizName);
    if (!quiz || quiz.privateKey !== privateKey) {
      return;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
601 602
    }

603
    quiz.setInactive();
604

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
605 606 607 608 609
    return {
      status: StatusProtocol.Success,
      step: MessageProtocol.Closed,
      payload: {},
    };
Fullarton's avatar
Fullarton committed
610 611
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
612 613 614 615
  @Post('/reset/:quizName')
  public resetQuiz(
    @Param('quizName') quizName: string, //
    @HeaderParam('authorization') privateKey: string, //
616 617
  ): object {

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
618 619 620 621
    if (!quizName || !privateKey) {
      throw new BadRequestError(JSON.stringify({
        status: StatusProtocol.Failed,
        step: MessageProtocol.InvalidParameters,
622
        payload: {},
Fullarton's avatar
Fullarton committed
623 624
      }));
    }
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
625 626

    const quiz = QuizDAO.getQuizByName(quizName);
627
    if (!quiz || quiz.privateKey !== privateKey) {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
628 629 630
      return;
    }

631
    DbDAO.updateOne(DbCollection.Quizzes, { _id: quiz.id }, {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
632 633 634
      state: QuizState.Active,
      currentQuestionIndex: -1,
      currentStartTimestamp: -1,
635
      readingConfirmationRequested: false,
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
636 637 638 639
    });

    const members = MemberDAO.getMembersOfQuiz(quizName);
    if (members.length > 0) {
640
      DbDAO.updateMany(DbCollection.Members, { currentQuizName: quizName }, {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
641 642 643 644 645 646
        responses: members[0].generateResponseForQuiz(quiz.questionList.length),
      });
    }

    quiz.reset();

647
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
648 649
      status: StatusProtocol.Success,
      step: MessageProtocol.Reset,
650
      payload: {},
651
    };
Fullarton's avatar
Fullarton committed
652 653
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
654 655 656
  @Get('/export/:quizName/:privateKey/:theme/:language') //
  @ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') //
  public async getExportFile(
657 658 659 660
    @Param('quizName') quizName: string, //
    @Param('privateKey') privateKey: string, //
    @Param('theme') themeName: string, //
    @Param('language') translation: string, //
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
661
    @Res() res: ICustomI18nResponse, //
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
662
  ): Promise<Buffer> {
663

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
664
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
Fullarton's avatar
Fullarton committed
665
    if (!activeQuiz) {
666
      return;
Fullarton's avatar
Fullarton committed
667
    }
668 669 670

    // TODO: The quiz contains the rewritten cached asset urls. Restore them to the original value!

Fullarton's avatar
Fullarton committed
671
    const wb = new ExcelWorkbook({
672 673
      themeName,
      translation,
674 675
      quiz: activeQuiz,
      mf: res.__mf,
Fullarton's avatar
Fullarton committed
676 677 678
    });
    const date: Date = new Date();
    const dateFormatted = `${date.getDate()}_${date.getMonth() + 1}_${date.getFullYear()}-${date.getHours()}_${date.getMinutes()}`;
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
679 680 681 682 683 684 685 686
    const name = `Export-${quizName}-${dateFormatted}.xlsx`;
    const buffer = await wb.writeToBuffer();

    res.header('Content-Disposition',
      'attachment; filename="' + encodeURIComponent(name) + '"; filename*=utf-8\'\'' + encodeURIComponent(name) + ';');
    res.header('Content-Length', buffer.length.toString());

    return buffer;
Fullarton's avatar
Fullarton committed
687 688
  }

689 690 691
  @Get('/member-group/:quizName')
  public getFreeMemberGroup(@Param('quizName') quizName: string): object {
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
692 693 694 695 696 697 698 699 700 701
    if (!activeQuiz) {
      return {
        status: StatusProtocol.Failed,
        step: MessageProtocol.GetFreeMemberGroup,
        payload: {},
      };
    }

    let groupName = 'Default';
    if (activeQuiz.sessionConfig.nicks.memberGroups.length > 1) {
702 703
      const memberGroupLoad = MemberDAO.getMemberAmountPerQuizGroup(activeQuiz.name);
      groupName = Object.entries(memberGroupLoad).sort((a, b) => a[1] - b[1])[0][0];
704 705 706 707 708 709 710 711 712 713 714
    }

    return {
      status: StatusProtocol.Success,
      step: MessageProtocol.GetFreeMemberGroup,
      payload: {
        groupName,
      },
    };
  }

715
  @Get('/leaderboard/:quizName/:amount/:questionIndex?')
716 717
  public getLeaderBoardData(
    @Param('quizName') quizName: string, //
718
    @Param('amount') amount: number, //
719
    @Param('questionIndex') questionIndex: number, //
720
    @HeaderParam('authorization') authorization: string, //
721
  ): object {
722

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
723
    const activeQuiz: IQuizEntity = QuizDAO.getActiveQuizByName(quizName);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
724
    if (!activeQuiz) {
725
      return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
726
        status: StatusProtocol.Failed,
727
        step: MessageProtocol.GetLeaderboardData,
728
        payload: {},
729
      };
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
730
    }
731
    const member = MemberDAO.getMemberByToken(authorization);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
732

733
    const { correctResponses, memberGroupResults } = this._leaderboard.buildLeaderboard(activeQuiz, questionIndex);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
734

735 736 737 738 739 740 741 742 743 744
    const sortedCorrectResponses = this._leaderboard.sortBy(correctResponses, 'score');
    const ownResponse: { [key: string]: any } = {};
    if (member) {
      ownResponse.element = sortedCorrectResponses.find(value => value.name === member.name);
      ownResponse.index = sortedCorrectResponses.indexOf(ownResponse.element);
      if (ownResponse.index > 0) {
        ownResponse.closestOpponent = sortedCorrectResponses[ownResponse.index - 1];
      }
    }

745
    return {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
746 747
      status: StatusProtocol.Success,
      step: MessageProtocol.GetLeaderboardData,
748
      payload: {
749
        correctResponses: sortedCorrectResponses.splice(0, amount),
750
        ownResponse,
751
        memberGroupResults: this._leaderboard.sortBy(memberGroupResults, 'score'),
752
      },
753
    };
Fullarton's avatar
Fullarton committed
754 755
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
756 757 758 759 760 761 762 763 764 765
  @Post('/private')
  private setQuizAsPrivate(@BodyParam('name') quizName: string, @HeaderParam('authorization') privateKey: string): void {
    const existingQuiz = QuizDAO.getQuizByName(quizName);
    if (!existingQuiz) {
      throw new NotFoundError(MessageProtocol.QuizNotFound);
    }
    if (existingQuiz.privateKey !== privateKey) {
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
    }

766
    DbDAO.updateOne(DbCollection.Quizzes, { _id: existingQuiz.id }, { visibility: QuizVisibility.Account });
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
767 768 769 770 771 772 773
  }

  @Get('/public')
  private getPublicQuizzes(@HeaderParam('authorization') privateKey: string): Array<IQuizSerialized> {
    return QuizDAO.getAllPublicQuizzes().filter(quiz => quiz.privateKey !== privateKey).map(quiz => quiz.serialize());
  }

774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
  @Post('/public/init')
  private async initQuizInstance(
    @BodyParam('name') quizName: string,
    @HeaderParam('X-Access-Token') loginToken: string,
    @HeaderParam('authorization') privateKey: string,
  ): Promise<IMessage> {
    const user = UserDAO.getUserByToken(loginToken);
    if (!user || !user.userAuthorizations.includes(UserRole.CreateQuiz)) {
      throw new UnauthorizedError('Unauthorized to create quiz');
    }

    const quiz = QuizDAO.getAllPublicQuizzes().find(q => q.name === quizName);
    if (!quiz) {
      throw new NotFoundError('Quiz name not found');
    }
    const serializedQuiz = quiz.serialize();

    delete quiz.id;
    serializedQuiz.name = QuizDAO.getRenameAsToken(serializedQuiz.name);
    serializedQuiz.privateKey = privateKey;
    serializedQuiz.state = QuizState.Active;
    serializedQuiz.visibility = QuizVisibility.Account;
    serializedQuiz.currentQuestionIndex = -1;
    serializedQuiz.currentStartTimestamp = -1;
    serializedQuiz.readingConfirmationRequested = false;

    const quizValidator = new QuizModel(serializedQuiz);
    const result = quizValidator.validateSync();
    if (result) {
      throw result;
    }
    await quizValidator.save();

    AMQPConnector.channel.publish('global', '.*', Buffer.from(JSON.stringify({
      status: StatusProtocol.Success,
      step: MessageProtocol.SetActive,
      payload: {
        quizName: serializedQuiz.name,
      },
    })));

    return {
      status: StatusProtocol.Success,
      step: MessageProtocol.Init,
      payload: {
        quiz: serializedQuiz,
      },
    };
  }

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
  @Get('/public/amount')
  private getPublicQuizAmount(@HeaderParam('authorization') privateKey: string): number {
    return this.getPublicQuizzes(privateKey).length;
  }

  @Get('/public/own')
  private getOwnPublicQuizzes(@HeaderParam('authorization') privateKey: string): Array<IQuizSerialized> {
    return QuizDAO.getAllPublicQuizzes().filter(quiz => quiz.privateKey === privateKey).map(quiz => quiz.serialize());
  }

  @Get('/public/amount/own')
  private getOwnPublicQuizAmount(@HeaderParam('authorization') privateKey: string): number {
    return this.getOwnPublicQuizzes(privateKey).length;
  }

839 840 841 842
  @Get('/')
  private getAll(): object {
    return {};
  }
843

844
  @Get('/quiz/:quizName?')
845 846 847
  private getQuiz(
    @Params() params: { [key: string]: any }, //
    @HeaderParam('authorization', { required: false }) token: string, //
848
  ): IMessage {
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873

    const quizName = params.quizName;
    const member = MemberDAO.getMemberByToken(token);

    if (!quizName && (!token || !member)) {
      throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
    }

    const quiz: IQuizEntity = QuizDAO.getQuizByName(quizName || member.currentQuizName);
    const payload: IQuizStatusPayload = {};

    if (quiz) {
      payload.state = quiz.state;
      payload.quiz = quiz.serialize();
    }

    return {
      status: StatusProtocol.Success,
      step: quiz ? [QuizState.Active, QuizState.Running].includes(quiz.state) ? MessageProtocol.Available : quiz.privateKey === token
                                                                                                            ? MessageProtocol.Editable
                                                                                                            : MessageProtocol.AlreadyTaken
                 : MessageProtocol.Unavailable,
      payload,
    };
  }
874
}