From 1174f61487b1f9138ec7828fdd27f4e5720aaec8 Mon Sep 17 00:00:00 2001 From: N08I40K Date: Sat, 25 Jan 2025 03:36:58 +0400 Subject: [PATCH] Refactor and reorganize codebase for better maintainability and clarity - Rename DTOs to entities and move them to appropriate directories - Remove deprecated controllers and services - Update imports and dependencies - Implement new class transformer decorators for better serialization - Add VK authentication support - Improve error handling and validation - Update ESLint configuration and TypeScript settings - Refactor schedule parsing logic - Enhance user and authentication services - Update Prisma schema and related entities - Improve code organization and structure This commit introduces significant changes to improve the overall structure and maintainability of the codebase, including better organization of DTOs, enhanced authentication features, and updated tooling configurations. --- .eslintrc.js | 25 - eslint.config.mjs | 42 + package.json | 84 +- prisma/schema.prisma | 3 +- src/app.module.ts | 1 - src/auth/auth-role.decorator.ts | 2 +- src/auth/auth.controller.ts | 158 +++ src/auth/auth.guard.ts | 35 +- src/auth/auth.module.ts | 5 +- src/auth/auth.pipe.ts | 4 +- src/auth/auth.service.ts | 203 +-- src/auth/dto/change-password.dto.ts | 2 +- src/auth/dto/sign-in-error.dto.ts | 15 + src/auth/dto/sign-in-response.dto.ts | 2 +- src/auth/dto/sign-in.dto.ts | 24 +- src/auth/dto/sign-up-error.dto.ts | 18 + src/auth/dto/sign-up-response.dto.ts | 7 +- src/auth/dto/sign-up.dto.ts | 93 +- src/auth/dto/update-token-response.dto.ts | 4 +- src/auth/dto/update-token.dto.ts | 4 +- src/auth/v1-auth.controller.ts | 138 -- src/auth/v2-auth.controller.ts | 93 -- src/contants.ts | 12 +- .../firebase-admin.controller.ts | 4 +- src/firebase-admin/firebase-admin.service.ts | 12 +- src/main.ts | 31 +- src/schedule/dto/cache-status.dto.ts | 2 +- ...-replacer.dto.ts => clear-replacer.dto.ts} | 2 +- ...up-names.dto.ts => get-group-names.dto.ts} | 2 +- src/schedule/dto/lesson.dto.ts | 112 -- ...hedule-replacer.dto.ts => replacer.dto.ts} | 4 +- src/schedule/dto/set-schedule-replacer.dto.ts | 2 +- src/schedule/dto/teacher-day.dto.ts | 15 - ...cher-names.dto.ts => teacher-names.dto.ts} | 2 +- src/schedule/dto/update-download-url.dto.ts | 2 +- .../day.dto.ts => entities/day.entity.ts} | 22 +- .../group-schedule.entity.ts} | 10 +- .../group.dto.ts => entities/group.entity.ts} | 8 +- .../lesson-sub-group.entity.ts} | 7 +- .../lesson-time.entity.ts} | 7 +- src/schedule/entities/lesson.entity.ts | 72 + .../schedule.entity.ts} | 8 +- src/schedule/entities/teacher-day.entity.ts | 46 + .../teacher-lesson.entity.ts} | 10 +- .../teacher-schedule.entity.ts} | 8 +- .../teacher.entity.ts} | 13 +- ...esson-type.enum.ts => lesson-type.enum.ts} | 2 +- ...parser.spec.ts => schedule-parser.spec.ts} | 23 +- ...-schedule-parser.ts => schedule-parser.ts} | 361 +++-- .../basic-xls-downloader.spec.ts | 2 +- .../xls-downloader/basic-xls-downloader.ts | 10 +- src/schedule/schedule-replacer.controller.ts | 18 +- src/schedule/schedule-replacer.service.ts | 2 +- ...e.controller.ts => schedule.controller.ts} | 98 +- src/schedule/schedule.module.ts | 11 +- src/schedule/schedule.service.ts | 71 +- src/schedule/v3-schedule.controller.ts | 105 -- src/schedule/v4-schedule.controller.ts | 128 -- src/users/dto/change-group.dto.ts | 12 +- src/users/dto/change-username.dto.ts | 14 +- src/users/dto/user.dto.ts | 70 + src/users/dto/v1/v1-client-user.dto.ts | 20 - src/users/dto/v2/v2-client-user.dto.ts | 20 - src/users/entity/fcm-user.entity.ts | 3 +- src/users/entity/user.entity.ts | 29 +- src/users/user-role.enum.ts | 4 +- ...sers.controller.ts => users.controller.ts} | 76 +- src/users/users.module.ts | 5 +- src/users/users.service.ts | 80 +- src/users/v2-users.controller.ts | 41 - .../class-transformer-ctor.ts | 33 + .../class-validators/conditional-field.ts | 13 +- .../validation/class-validator.interceptor.ts | 42 +- src/version/response-version.decorator.ts | 13 - test/mainPage | 1215 ----------------- tsconfig.json | 1 + 76 files changed, 1273 insertions(+), 2624 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 eslint.config.mjs create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/dto/sign-in-error.dto.ts create mode 100644 src/auth/dto/sign-up-error.dto.ts delete mode 100644 src/auth/v1-auth.controller.ts delete mode 100644 src/auth/v2-auth.controller.ts rename src/schedule/dto/{clear-schedule-replacer.dto.ts => clear-replacer.dto.ts} (82%) rename src/schedule/dto/{schedule-group-names.dto.ts => get-group-names.dto.ts} (79%) delete mode 100644 src/schedule/dto/lesson.dto.ts rename src/schedule/dto/{schedule-replacer.dto.ts => replacer.dto.ts} (61%) delete mode 100644 src/schedule/dto/teacher-day.dto.ts rename src/schedule/dto/{schedule-teacher-names.dto.ts => teacher-names.dto.ts} (79%) rename src/schedule/{dto/day.dto.ts => entities/day.entity.ts} (51%) rename src/schedule/{dto/group-schedule.dto.ts => entities/group-schedule.entity.ts} (68%) rename src/schedule/{dto/group.dto.ts => entities/group.entity.ts} (75%) rename src/schedule/{dto/lesson-sub-group.dto.ts => entities/lesson-sub-group.entity.ts} (67%) rename src/schedule/{dto/lesson-time.dto.ts => entities/lesson-time.entity.ts} (59%) create mode 100644 src/schedule/entities/lesson.entity.ts rename src/schedule/{dto/schedule.dto.ts => entities/schedule.entity.ts} (83%) create mode 100644 src/schedule/entities/teacher-day.entity.ts rename src/schedule/{dto/teacher-lesson.dto.ts => entities/teacher-lesson.entity.ts} (53%) rename src/schedule/{dto/teacher-schedule.dto.ts => entities/teacher-schedule.entity.ts} (70%) rename src/schedule/{dto/teacher.dto.ts => entities/teacher.entity.ts} (54%) rename src/schedule/enum/{v2-lesson-type.enum.ts => lesson-type.enum.ts} (91%) rename src/schedule/internal/schedule-parser/{v2-schedule-parser.spec.ts => schedule-parser.spec.ts} (76%) rename src/schedule/internal/schedule-parser/{v2-schedule-parser.ts => schedule-parser.ts} (66%) rename src/schedule/{v2-schedule.controller.ts => schedule.controller.ts} (63%) delete mode 100644 src/schedule/v3-schedule.controller.ts delete mode 100644 src/schedule/v4-schedule.controller.ts create mode 100644 src/users/dto/user.dto.ts delete mode 100644 src/users/dto/v1/v1-client-user.dto.ts delete mode 100644 src/users/dto/v2/v2-client-user.dto.ts rename src/users/{v1-users.controller.ts => users.controller.ts} (52%) delete mode 100644 src/users/v2-users.controller.ts create mode 100644 src/utility/class-trasformer/class-transformer-ctor.ts delete mode 100644 src/version/response-version.decorator.ts delete mode 100644 test/mainPage diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 8924071..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - sourceType: 'module', - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - root: true, - env: { - node: true, - jest: false, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - }, -}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..bb5c8e8 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,42 @@ +// @ts-check +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["eslint.config.mjs", "**/node_modules/"], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + ecmaVersion: 5, + sourceType: "module", + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/no-unused-vars": "warn", + }, + }, +); diff --git a/package.json b/package.json index 35942f3..fc3a79b 100644 --- a/package.json +++ b/package.json @@ -8,74 +8,78 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\"", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "precommit": "npm run format && npm run lint", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", - "precommit": "npm run format && npm run lint" + "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs/cache-manager": "^2.2.2", - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.4.5", - "@nestjs/jwt": "^10.2.0", - "@nestjs/platform-express": "^10.4.4", - "@nestjs/swagger": "^7.4.2", - "@prisma/client": "^5.19.1", - "axios": "^1.7.7", + "@fastify/static": "^8.0.4", + "@nestjs/cache-manager": "^3.0.0", + "@nestjs/common": "^11.0.5", + "@nestjs/core": "^11.0.5", + "@nestjs/jwt": "^11.0.0", + "@nestjs/platform-express": "^11.0.5", + "@nestjs/platform-fastify": "^11.0.5", + "@nestjs/swagger": "^11.0.3", + "@prisma/client": "^6.2.1", + "axios": "^1.7.9", "bcrypt": "^5.1.1", - "cache-manager": "^5.7.6", + "bson-objectid": "^2.0.4", + "cache-manager": "^6.4.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", - "cookie": ">=0.7.0", + "cookie": ">=1.0.2", "create-map-transform-fn": "gist:f65ddd8f17f8c388659aab76890f194b", - "dotenv": "^16.4.5", - "firebase-admin": "^12.6.0", - "jsdom": "^25.0.0", - "mongoose": "^8.6.1", - "nest-redoc": "^1.1.2", + "dotenv": "^16.4.7", + "firebase-admin": "^13.0.2", + "jsdom": "^26.0.0", "object-hash": "^3.0.0", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.1", - "uuid": "^10.0.0", + "uuid": "^11.0.5", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", + "@eslint/eslintrc": "3.2.0", + "@eslint/js": "9.18.0", + "@nestjs/cli": "^11.0.2", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.5", + "@swc/cli": "^0.6.0", + "@swc/core": "^1.10.9", "@types/bcrypt": "^5.0.2", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", "@types/jsdom": "^21.1.7", "@types/multer": "^1.4.12", - "@types/node": "^20.16.5", + "@types/node": "^22.10.9", "@types/object-hash": "^3.0.6", - "@types/supertest": "^6.0.0", + "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "cookie": ">=0.7.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.1.0", - "prettier": "^3.0.0", - "prisma": "^5.19.1", + "cookie": ">=1.0.2", + "eslint": "9.18.0", + "eslint-plugin-prettier": "5.2.3", + "fastify": "^5.2.1", + "jest": "^29.7.0", + "prettier": "3.4.2", + "prisma": "^6.2.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", - "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.7.3", + "typescript-eslint": "8.21.0" }, "jest": { "moduleFileExtensions": [ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1202cef..8d719db 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,9 +35,10 @@ model User { // username String @unique // - salt String password String // + vkId Int? + // accessToken String @unique // group String diff --git a/src/app.module.ts b/src/app.module.ts index b2aaea5..b0e9eec 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,7 +13,6 @@ import { FirebaseAdminModule } from "./firebase-admin/firebase-admin.module"; CacheModule.register({ ttl: 5 * 60 * 1000, isGlobal: true }), FirebaseAdminModule, ], - controllers: [], providers: [], }) export class AppModule {} diff --git a/src/auth/auth-role.decorator.ts b/src/auth/auth-role.decorator.ts index 33c81b2..d03f27f 100644 --- a/src/auth/auth-role.decorator.ts +++ b/src/auth/auth-role.decorator.ts @@ -1,6 +1,6 @@ import { Reflector } from "@nestjs/core"; -import { UserRole } from "../users/user-role.enum"; +import UserRole from "../users/user-role.enum"; export const AuthRoles = Reflector.createDecorator(); export const AuthUnauthorized = Reflector.createDecorator(); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..1bccbff --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,158 @@ +import { + Body, + Controller, + HttpCode, + HttpStatus, + Post, + Res, +} from "@nestjs/common"; +import { AuthService } from "./auth.service"; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { ResultDto } from "../utility/validation/class-validator.interceptor"; +import { SignInDto, SignInVKDto } from "./dto/sign-in.dto"; +import { SignUpDto, SignUpVKDto } from "./dto/sign-up.dto"; +import { ScheduleService } from "../schedule/schedule.service"; +import SignInErrorDto from "./dto/sign-in-error.dto"; +import { FastifyReply } from "fastify"; +import SignUpErrorDto, { SignUpErrorCode } from "./dto/sign-up-error.dto"; +import UserDto from "src/users/dto/user.dto"; +import GetGroupNamesDto from "../schedule/dto/get-group-names.dto"; + +@ApiTags("v2/auth") +@Controller({ path: "auth", version: "1" }) +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly scheduleService: ScheduleService, + ) {} + + @ApiOperation({ summary: "Авторизация по логину и паролю" }) + @ApiBody({ type: SignInDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: "Авторизация прошла успешно", + type: UserDto, + }) + @ApiResponse({ + status: HttpStatus.NOT_ACCEPTABLE, + description: "Переданы неверные входные данные", + type: SignInErrorDto, + }) + @ResultDto([UserDto, SignInErrorDto]) + @HttpCode(HttpStatus.OK) + @Post("sign-in") + async signIn( + @Body() signInDto: SignInDto, + @Res({ passthrough: true }) response: FastifyReply, + ): Promise { + const result = await this.authService.signIn(signInDto); + + if (result instanceof SignInErrorDto) + response.status(HttpStatus.NOT_ACCEPTABLE); + + return result; + } + + @ApiOperation({ summary: "Регистрация по логину и паролю" }) + @ApiBody({ type: SignUpDto }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: "Регистрация прошла успешно", + type: UserDto, + }) + @ApiResponse({ + status: HttpStatus.NOT_ACCEPTABLE, + description: "Переданы неверные входные данные", + type: SignUpErrorDto, + }) + @ResultDto([UserDto, SignUpErrorDto]) + @HttpCode(HttpStatus.CREATED) + @Post("sign-up") + async signUp( + @Body() signUpDto: SignUpDto, + @Res({ passthrough: true }) response: FastifyReply, + ): Promise { + const groupNames = await this.scheduleService + .getGroupNames() + .catch((): GetGroupNamesDto => null); + + if ( + groupNames && + !groupNames.names.includes(signUpDto.group.replaceAll(" ", "")) + ) { + response.status(HttpStatus.NOT_ACCEPTABLE); + return new SignUpErrorDto(SignUpErrorCode.INVALID_GROUP_NAME); + } + + const result = await this.authService.signUp(signUpDto); + if (result instanceof SignUpErrorDto) + response.status(HttpStatus.NOT_ACCEPTABLE); + + return result; + } + + @ApiOperation({ summary: "Авторизация с помощью VK ID" }) + @ApiBody({ type: SignInVKDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: "Авторизация прошла успешно", + type: UserDto, + }) + @ApiResponse({ + status: HttpStatus.NOT_ACCEPTABLE, + description: "Переданы неверные входные данные", + type: SignInErrorDto, + }) + @ResultDto([UserDto, SignInErrorDto]) + @HttpCode(HttpStatus.OK) + @Post("sign-in-vk") + async signInVK( + @Body() signInVKDto: SignInVKDto, + @Res({ passthrough: true }) response: FastifyReply, + ): Promise { + const result = await this.authService.signInVK(signInVKDto); + + if (result instanceof SignInErrorDto) + response.status(HttpStatus.NOT_ACCEPTABLE); + + return result; + } + + @ApiOperation({ summary: "Регистрация с помощью VK ID" }) + @ApiBody({ type: SignUpVKDto }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: "Регистрация прошла успешно", + type: UserDto, + }) + @ApiResponse({ + status: HttpStatus.NOT_ACCEPTABLE, + description: "Переданы неверные входные данные", + type: SignUpErrorDto, + }) + @ResultDto([UserDto, SignUpErrorDto]) + @HttpCode(HttpStatus.CREATED) + @Post("sign-up-vk") + async signUpVK( + @Body() signUpVKDto: SignUpVKDto, + @Res({ passthrough: true }) response: FastifyReply, + ): Promise { + const groupNames = await this.scheduleService + .getGroupNames() + .catch((): GetGroupNamesDto => null); + + if ( + groupNames && + !groupNames.names.includes(signUpVKDto.group.replaceAll(" ", "")) + ) { + response.status(HttpStatus.NOT_ACCEPTABLE); + return new SignUpErrorDto(SignUpErrorCode.INVALID_GROUP_NAME); + } + + const result = await this.authService.signUpVK(signUpVKDto); + if (result instanceof SignUpErrorDto) + response.status(HttpStatus.NOT_ACCEPTABLE); + + return result; + } +} diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index b12c6f7..9c14bb3 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -1,16 +1,19 @@ import { CanActivate, ExecutionContext, - ForbiddenException, Injectable, UnauthorizedException, } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; -import { Request } from "express"; import { UsersService } from "../users/users.service"; import { Reflector } from "@nestjs/core"; import { AuthRoles, AuthUnauthorized } from "./auth-role.decorator"; import { isJWT } from "class-validator"; +import { FastifyRequest } from "fastify"; + +interface JWTUser { + id: string; +} @Injectable() export class AuthGuard implements CanActivate { @@ -20,11 +23,10 @@ export class AuthGuard implements CanActivate { private readonly reflector: Reflector, ) {} - public static extractTokenFromRequest(req: Request): string { + public static extractTokenFromRequest(req: FastifyRequest): string { const [type, token] = req.headers.authorization?.split(" ") ?? []; - if (type !== "Bearer" || !token || token.length === 0) - throw new UnauthorizedException("Не указан токен!"); + if (type !== "Bearer" || !token || token.length === 0) return null; return token; } @@ -33,31 +35,24 @@ export class AuthGuard implements CanActivate { if (this.reflector.get(AuthUnauthorized, context.getHandler())) return true; - const request = context.switchToHttp().getRequest(); + const request: FastifyRequest = context.switchToHttp().getRequest(); const token = AuthGuard.extractTokenFromRequest(request); - let jwtUser: { id: string } | null = null; + if (!token || !isJWT(token)) throw new UnauthorizedException(); - if ( - !isJWT(token) || - !(jwtUser = await this.jwtService - .verifyAsync(token) - .catch(() => null)) - ) - throw new UnauthorizedException(); + const jwtUser = await this.jwtService + .verifyAsync(token) + .catch((): JWTUser => null); + if (!jwtUser) throw new UnauthorizedException(); const user = await this.usersService.findUnique({ id: jwtUser.id }); - if (!user || user.accessToken !== token) - throw new UnauthorizedException(); + if (!user) throw new UnauthorizedException(); const acceptableRoles = this.reflector.get( AuthRoles, context.getHandler(), ); - if (acceptableRoles != null && !acceptableRoles.includes(user.role)) - throw new ForbiddenException(); - - return true; + return !(acceptableRoles && !acceptableRoles.includes(user.role)); } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 3f4bbe1..dfc1792 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,11 +2,10 @@ import { forwardRef, Module } from "@nestjs/common"; import { JwtModule } from "@nestjs/jwt"; import { jwtConstants } from "../contants"; import { AuthService } from "./auth.service"; -import { V1AuthController } from "./v1-auth.controller"; import { UsersModule } from "../users/users.module"; import { PrismaService } from "../prisma/prisma.service"; import { ScheduleModule } from "../schedule/schedule.module"; -import { V2AuthController } from "./v2-auth.controller"; +import { AuthController } from "./auth.controller"; @Module({ imports: [ @@ -19,7 +18,7 @@ import { V2AuthController } from "./v2-auth.controller"; }), ], providers: [AuthService, PrismaService], - controllers: [V1AuthController, V2AuthController], + controllers: [AuthController], exports: [AuthService], }) export class AuthModule {} diff --git a/src/auth/auth.pipe.ts b/src/auth/auth.pipe.ts index 04103a8..efa710a 100644 --- a/src/auth/auth.pipe.ts +++ b/src/auth/auth.pipe.ts @@ -6,7 +6,7 @@ import { import { JwtService } from "@nestjs/jwt"; import { UsersService } from "../users/users.service"; -import { User } from "../users/entity/user.entity"; +import User from "../users/entity/user.entity"; @Injectable() export class UserPipe implements PipeTransform { @@ -25,6 +25,6 @@ export class UserPipe implements PipeTransform { if (!user) throw new UnauthorizedException("Передан некорректный токен!"); - return user as User; + return user; } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 56347b9..ba8fdb3 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,20 +1,21 @@ import { ConflictException, Injectable, - NotAcceptableException, - NotFoundException, UnauthorizedException, } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; import { UsersService } from "../users/users.service"; -import { genSalt, hash } from "bcrypt"; -import { Prisma } from "@prisma/client"; -import { Types } from "mongoose"; -import { UserRole } from "../users/user-role.enum"; -import { User } from "../users/entity/user.entity"; -import { SignInDto } from "./dto/sign-in.dto"; -import { SignUpDto } from "./dto/sign-up.dto"; -import { ChangePasswordDto } from "./dto/change-password.dto"; +import { compare, genSalt, hash } from "bcrypt"; +import UserRole from "../users/user-role.enum"; +import User from "../users/entity/user.entity"; +import ChangePasswordDto from "./dto/change-password.dto"; +import axios from "axios"; +import SignInErrorDto, { SignInErrorCode } from "./dto/sign-in-error.dto"; +import { SignUpDto, SignUpVKDto } from "./dto/sign-up.dto"; +import SignUpErrorDto, { SignUpErrorCode } from "./dto/sign-up-error.dto"; +import { SignInDto, SignInVKDto } from "./dto/sign-in.dto"; +import ObjectID from "bson-objectid"; +import UserDto from "../users/dto/user.dto"; @Injectable() export class AuthService { @@ -51,110 +52,122 @@ export class AuthService { return user; } - /** - * Регистрация нового пользователя - * @param signUp - данные нового пользователя - * @returns {User} - пользователь - * @throws {NotAcceptableException} - передана недопустимая роль - * @throws {ConflictException} - пользователь с таким именем уже существует - * @async - */ - async signUp(signUp: SignUpDto): Promise { - const group = signUp.group.replaceAll(" ", ""); - const username = signUp.username.trim(); + async signUp(signUpDto: SignUpDto): Promise { + if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUpDto.role)) + return new SignUpErrorDto(SignUpErrorCode.DISALLOWED_ROLE); - if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUp.role)) - throw new NotAcceptableException("Передана неизвестная роль"); + if (await this.usersService.contains({ username: signUpDto.username })) + return new SignUpErrorDto(SignUpErrorCode.USERNAME_ALREADY_EXISTS); - if (await this.usersService.contains({ username: username })) { - throw new ConflictException( - "Пользователь с таким именем уже существует!", - ); - } + const id = ObjectID().toHexString(); - const salt = await genSalt(8); - const id = new Types.ObjectId().toString("hex"); - - const input: Prisma.UserCreateInput = { - id: id, - username: username, - salt: salt, - password: await hash(signUp.password, salt), - accessToken: await this.jwtService.signAsync({ + return UserDto.fromPlain( + await this.usersService.create({ id: id, + username: signUpDto.username, + password: await hash(signUpDto.password, await genSalt(8)), + accessToken: await this.jwtService.signAsync({ id: id }), + group: signUpDto.group, + role: signUpDto.role, + version: signUpDto.version, }), - role: signUp.role as UserRole, - group: group, - version: signUp.version ?? "1.0.0", - }; - - return await this.usersService.create(input); + ["auth"], + ); } - /** - * Авторизация пользователя - * @param signIn - данные авторизации - * @returns {User} - пользователь - * @throws {UnauthorizedException} - некорректное имя пользователя или пароль - * @async - */ - async signIn(signIn: SignInDto): Promise { + async signIn(signIn: SignInDto): Promise { const user = await this.usersService.findUnique({ username: signIn.username, }); - if ( - !user || - user.password !== (await hash(signIn.password, user.salt)) - ) { - throw new UnauthorizedException( - "Некорректное имя пользователя или пароль!", - ); - } + if (!user || !(await compare(signIn.password, user.password))) + return new SignInErrorDto(SignInErrorCode.INCORRECT_CREDENTIALS); - const accessToken = await this.jwtService.signAsync({ id: user.id }); - - return await this.usersService.update({ - where: { id: user.id }, - data: { accessToken: accessToken }, - }); + return UserDto.fromPlain( + await this.usersService.update({ + where: { id: user.id }, + data: { + accessToken: await this.jwtService.signAsync({ + id: user.id, + }), + }, + }), + ["auth"], + ); } - /** - * Обновление токена пользователя - * @param oldToken - старый токен - * @returns {User} - пользователь - * @throws {NotFoundException} - некорректный или недействительный токен - * @throws {NotFoundException} - токен указывает на несуществующего пользователя - * @throws {NotFoundException} - текущий токен устарел и был обновлён на новый - * @async - */ - async updateToken(oldToken: string): Promise { - if ( - !(await this.jwtService.verifyAsync(oldToken, { - ignoreExpiration: true, - })) - ) { - throw new NotFoundException( - "Некорректный или недействительный токен!", - ); - } + private static async parseVKID(accessToken: string): Promise { + const form = new FormData(); + form.append("access_token", accessToken); + form.append("v", "5.199"); - const jwtUser: { id: string } = await this.jwtService.decode(oldToken); + const response = await axios.post( + "https://api.vk.com/method/account.getProfileInfo", + form, + { responseType: "json" }, + ); - const user = await this.usersService.findUnique({ id: jwtUser.id }); - if (!user || user.accessToken !== oldToken) { - throw new NotFoundException( - "Некорректный или недействительный токен!", - ); - } + const data: { error?: any; response?: { id: number } } = + response.data as object; + + if (response.status !== 200 || data.error !== undefined) return null; + + return data.response.id; + } + + async signUpVK(signUpDto: SignUpVKDto): Promise { + if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUpDto.role)) + return new SignUpErrorDto(SignUpErrorCode.DISALLOWED_ROLE); + + if (await this.usersService.contains({ username: signUpDto.username })) + return new SignUpErrorDto(SignUpErrorCode.USERNAME_ALREADY_EXISTS); + + const vkId = await AuthService.parseVKID(signUpDto.accessToken); + if (!vkId) + return new SignUpErrorDto(SignUpErrorCode.INVALID_VK_ACCESS_TOKEN); + + if (await this.usersService.contains({ vkId: vkId })) + return new SignUpErrorDto(SignUpErrorCode.VK_ALREADY_EXISTS); + + const id = ObjectID().toHexString(); + + return UserDto.fromPlain( + await this.usersService.create({ + id: id, + username: signUpDto.username, + password: await hash(await genSalt(8), await genSalt(8)), + vkId: vkId, + accessToken: await this.jwtService.signAsync({ + id: id, + }), + role: signUpDto.role, + group: signUpDto.group, + version: signUpDto.version, + }), + ["auth"], + ); + } + + async signInVK( + signInVKDto: SignInVKDto, + ): Promise { + const vkId = await AuthService.parseVKID(signInVKDto.accessToken); + if (!vkId) + return new SignInErrorDto(SignInErrorCode.INVALID_VK_ACCESS_TOKEN); + + const user = await this.usersService.findOne({ vkId: vkId }); + if (!user) + return new SignInErrorDto(SignInErrorCode.INCORRECT_CREDENTIALS); const accessToken = await this.jwtService.signAsync({ id: user.id }); - return await this.usersService.update({ - where: { id: user.id }, - data: { accessToken: accessToken }, - }); + return UserDto.fromPlain( + await this.usersService.update({ + where: { id: user.id }, + data: { accessToken: accessToken }, + }), + ["auth"], + ); } /** diff --git a/src/auth/dto/change-password.dto.ts b/src/auth/dto/change-password.dto.ts index 1bc8639..fa67d94 100644 --- a/src/auth/dto/change-password.dto.ts +++ b/src/auth/dto/change-password.dto.ts @@ -1,6 +1,6 @@ import { IsString } from "class-validator"; -export class ChangePasswordDto { +export default class ChangePasswordDto { /** * Старый пароль * @example "my-old-password" diff --git a/src/auth/dto/sign-in-error.dto.ts b/src/auth/dto/sign-in-error.dto.ts new file mode 100644 index 0000000..4eb0f37 --- /dev/null +++ b/src/auth/dto/sign-in-error.dto.ts @@ -0,0 +1,15 @@ +import { IsEnum } from "class-validator"; + +export enum SignInErrorCode { + INCORRECT_CREDENTIALS = "INCORRECT_CREDENTIALS", + INVALID_VK_ACCESS_TOKEN = "INVALID_VK_ACCESS_TOKEN", +} + +export default class SignInErrorDto { + @IsEnum(SignInErrorCode) + code: SignInErrorCode; + + constructor(errorCode: SignInErrorCode) { + this.code = errorCode; + } +} diff --git a/src/auth/dto/sign-in-response.dto.ts b/src/auth/dto/sign-in-response.dto.ts index 78bb6f4..2727d1c 100644 --- a/src/auth/dto/sign-in-response.dto.ts +++ b/src/auth/dto/sign-in-response.dto.ts @@ -1,6 +1,6 @@ import { IsJWT, IsMongoId, IsOptional, IsString } from "class-validator"; -export class SignInResponseDto { +export default class SignInResponseDto { /** * Идентификатор (ObjectId) * @example "66e1b7e255c5d5f1268cce90" diff --git a/src/auth/dto/sign-in.dto.ts b/src/auth/dto/sign-in.dto.ts index 20d823d..bd1b774 100644 --- a/src/auth/dto/sign-in.dto.ts +++ b/src/auth/dto/sign-in.dto.ts @@ -1,8 +1,15 @@ -import { PickType } from "@nestjs/swagger"; -import { User } from "../../users/entity/user.entity"; -import { IsString } from "class-validator"; +import { IsString, MaxLength, MinLength } from "class-validator"; + +export class SignInDto { + /** + * Имя + * @example "n08i40k" + */ + @IsString() + @MinLength(1) + @MaxLength(20) + username: string; -export class SignInDto extends PickType(User, ["username"]) { /** * Пароль в исходном виде * @example "my-password" @@ -10,3 +17,12 @@ export class SignInDto extends PickType(User, ["username"]) { @IsString() password: string; } + +export class SignInVKDto { + /** + * Токен VK + * @example "хз" + */ + @IsString() + accessToken: string; +} diff --git a/src/auth/dto/sign-up-error.dto.ts b/src/auth/dto/sign-up-error.dto.ts new file mode 100644 index 0000000..7c72525 --- /dev/null +++ b/src/auth/dto/sign-up-error.dto.ts @@ -0,0 +1,18 @@ +import { IsEnum } from "class-validator"; + +export enum SignUpErrorCode { + USERNAME_ALREADY_EXISTS = "USERNAME_ALREADY_EXISTS", + VK_ALREADY_EXISTS = "VK_ALREADY_EXISTS", + INVALID_VK_ACCESS_TOKEN = "INVALID_VK_ACCESS_TOKEN", + INVALID_GROUP_NAME = "INVALID_GROUP_NAME", + DISALLOWED_ROLE = "DISALLOWED_ROLE", +} + +export default class SignUpErrorDto { + @IsEnum(SignUpErrorCode) + code: SignUpErrorCode; + + constructor(errorCode: SignUpErrorCode) { + this.code = errorCode; + } +} diff --git a/src/auth/dto/sign-up-response.dto.ts b/src/auth/dto/sign-up-response.dto.ts index cde869e..b0a888d 100644 --- a/src/auth/dto/sign-up-response.dto.ts +++ b/src/auth/dto/sign-up-response.dto.ts @@ -1,4 +1,7 @@ import { PickType } from "@nestjs/swagger"; -import { User } from "../../users/entity/user.entity"; +import User from "../../users/entity/user.entity"; -export class SignUpResponseDto extends PickType(User, ["id", "accessToken"]) {} +export default class SignUpResponseDto extends PickType(User, [ + "id", + "accessToken", +]) {} diff --git a/src/auth/dto/sign-up.dto.ts b/src/auth/dto/sign-up.dto.ts index 285e629..8e60410 100644 --- a/src/auth/dto/sign-up.dto.ts +++ b/src/auth/dto/sign-up.dto.ts @@ -1,9 +1,86 @@ -import { IntersectionType, PartialType, PickType } from "@nestjs/swagger"; -import { SignInDto } from "./sign-in.dto"; -import { User } from "../../users/entity/user.entity"; +import { + IsEnum, + IsSemVer, + IsString, + MaxLength, + MinLength, +} from "class-validator"; +import UserRole from "../../users/user-role.enum"; -export class SignUpDto extends IntersectionType( - SignInDto, - PickType(User, ["role", "group"]), - PartialType(PickType(User, ["version"])), -) {} +export class SignUpDto { + /** + * Имя + * @example "n08i40k" + */ + @IsString() + @MinLength(1) + @MaxLength(20) + username: string; + + /** + * Пароль в исходном виде + * @example "my-password" + */ + @IsString() + password: string; + + /** + * Группа + * @example "ИС-214/23" + */ + @IsString() + group: string; + + /** + * Роль + * @example STUDENT + */ + @IsEnum(UserRole) + role: UserRole; + + /** + * Версия установленного приложения + * @example "2.0.0" + */ + @IsSemVer() + version: string; +} + +export class SignUpVKDto { + /** + * Токен VK + * @example "хз" + */ + @IsString() + accessToken: string; + + /** + * Имя + * @example "n08i40k" + */ + @IsString() + @MinLength(1) + @MaxLength(20) + username: string; + + /** + * Группа + * @example "ИС-214/23" + */ + @IsString() + group: string; + + /** + * Роль + * @example STUDENT + */ + @IsEnum(UserRole) + role: UserRole; + + /** + * Версия установленного приложения + * @example "2.0.0" + */ + @IsSemVer() + version: string; +} diff --git a/src/auth/dto/update-token-response.dto.ts b/src/auth/dto/update-token-response.dto.ts index d7dc03f..306752d 100644 --- a/src/auth/dto/update-token-response.dto.ts +++ b/src/auth/dto/update-token-response.dto.ts @@ -1,3 +1,3 @@ -import { UpdateTokenDto } from "./update-token.dto"; +import UpdateTokenDto from "./update-token.dto"; -export class UpdateTokenResponseDto extends UpdateTokenDto {} +export default class UpdateTokenResponseDto extends UpdateTokenDto {} diff --git a/src/auth/dto/update-token.dto.ts b/src/auth/dto/update-token.dto.ts index b7fe46a..a235577 100644 --- a/src/auth/dto/update-token.dto.ts +++ b/src/auth/dto/update-token.dto.ts @@ -1,4 +1,4 @@ import { PickType } from "@nestjs/swagger"; -import { User } from "../../users/entity/user.entity"; +import User from "../../users/entity/user.entity"; -export class UpdateTokenDto extends PickType(User, ["accessToken"]) {} +export default class UpdateTokenDto extends PickType(User, ["accessToken"]) {} diff --git a/src/auth/v1-auth.controller.ts b/src/auth/v1-auth.controller.ts deleted file mode 100644 index 794e263..0000000 --- a/src/auth/v1-auth.controller.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - Body, - Controller, - HttpCode, - HttpStatus, - NotFoundException, - Post, -} from "@nestjs/common"; -import { AuthService } from "./auth.service"; -import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { ResultDto } from "../utility/validation/class-validator.interceptor"; -import { UserToken } from "./auth.decorator"; -import { ResponseVersion } from "../version/response-version.decorator"; -import { SignInDto } from "./dto/sign-in.dto"; -import { SignInResponseDto } from "./dto/sign-in-response.dto"; -import { SignUpResponseDto } from "./dto/sign-up-response.dto"; -import { SignUpDto } from "./dto/sign-up.dto"; -import { UpdateTokenDto } from "./dto/update-token.dto"; -import { UpdateTokenResponseDto } from "./dto/update-token-response.dto"; -import { ChangePasswordDto } from "./dto/change-password.dto"; -import { ScheduleService } from "../schedule/schedule.service"; - -@ApiTags("v1/auth") -@Controller({ path: "auth", version: "1" }) -export class V1AuthController { - constructor( - private readonly authService: AuthService, - private readonly scheduleService: ScheduleService, - ) {} - - @ApiOperation({ summary: "Авторизация по логину и паролю" }) - @ApiBody({ type: SignInDto }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Авторизация прошла успешно", - type: SignInResponseDto, - }) - @ApiResponse({ - status: HttpStatus.UNAUTHORIZED, - description: "Некорректное имя пользователя или пароль", - }) - @ResultDto(SignInResponseDto) - @HttpCode(HttpStatus.OK) - @Post("sign-in") - async signIn( - @Body() signInDto: SignInDto, - @ResponseVersion() responseVersion: number, - ): Promise { - const data = await this.authService.signIn(signInDto); - - return { - id: data.id, - accessToken: data.accessToken, - group: responseVersion ? data.group : null, - }; - } - - @ApiOperation({ summary: "Регистрация по логину и паролю" }) - @ApiBody({ type: SignUpDto }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: "Регистрация прошла успешно", - type: SignUpResponseDto, - }) - @ApiResponse({ - status: HttpStatus.CONFLICT, - description: "Такой пользователь уже существует", - }) - @ResultDto(SignUpResponseDto) - @HttpCode(HttpStatus.CREATED) - @Post("sign-up") - async signUp(@Body() signUpDto: SignUpDto): Promise { - if ( - !(await this.scheduleService.getGroupNames()).names.includes( - signUpDto.group.replaceAll(" ", ""), - ) - ) { - throw new NotFoundException( - "Передано название несуществующей группы", - ); - } - - const user = await this.authService.signUp(signUpDto); - return { - id: user.id, - accessToken: user.accessToken, - }; - } - - @ApiOperation({ summary: "Обновление просроченного токена" }) - @ApiBody({ type: UpdateTokenDto }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Токен обновлён успешно", - type: UpdateTokenResponseDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: "Передан несуществующий или недействительный токен", - }) - @ResultDto(UpdateTokenResponseDto) - @HttpCode(HttpStatus.OK) - @Post("update-token") - updateToken( - @Body() updateTokenDto: UpdateTokenDto, - ): Promise { - return this.authService.updateToken(updateTokenDto.accessToken); - } - - @ApiOperation({ summary: "Обновление пароля" }) - @ApiBody({ type: ChangePasswordDto }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Пароль обновлён успешно", - }) - @ApiResponse({ - status: HttpStatus.CONFLICT, - description: "Пароли идентичны", - }) - @ApiResponse({ - status: HttpStatus.UNAUTHORIZED, - description: - "Передан неверный текущий пароль или запрос был послан без токена", - }) - @ResultDto(null) - @HttpCode(HttpStatus.OK) - @Post("change-password") - async changePassword( - @Body() changePasswordReqDto: ChangePasswordDto, - @UserToken() userToken: string, - ): Promise { - await this.authService - .decodeUserToken(userToken) - .then((user) => - this.authService.changePassword(user, changePasswordReqDto), - ); - } -} diff --git a/src/auth/v2-auth.controller.ts b/src/auth/v2-auth.controller.ts deleted file mode 100644 index 7b319e4..0000000 --- a/src/auth/v2-auth.controller.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - Body, - Controller, - HttpCode, - HttpStatus, - NotFoundException, - Post, -} from "@nestjs/common"; -import { AuthService } from "./auth.service"; -import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { ResultDto } from "../utility/validation/class-validator.interceptor"; -import { SignInDto } from "./dto/sign-in.dto"; -import { SignUpDto } from "./dto/sign-up.dto"; -import { UpdateTokenDto } from "./dto/update-token.dto"; -import { V2ClientUserDto } from "../users/dto/v2/v2-client-user.dto"; -import { ScheduleService } from "../schedule/schedule.service"; - -@ApiTags("v2/auth") -@Controller({ path: "auth", version: "2" }) -export class V2AuthController { - constructor( - private readonly authService: AuthService, - private readonly scheduleService: ScheduleService, - ) {} - - @ApiOperation({ summary: "Авторизация по логину и паролю" }) - @ApiBody({ type: SignInDto }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Авторизация прошла успешно", - type: V2ClientUserDto, - }) - @ApiResponse({ - status: HttpStatus.UNAUTHORIZED, - description: "Некорректное имя пользователя или пароль", - }) - @ResultDto(V2ClientUserDto) - @HttpCode(HttpStatus.OK) - @Post("sign-in") - async signIn(@Body() reqDto: SignInDto): Promise { - return V2ClientUserDto.fromUser(await this.authService.signIn(reqDto)); - } - - @ApiOperation({ summary: "Регистрация по логину и паролю" }) - @ApiBody({ type: SignUpDto }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: "Регистрация прошла успешно", - type: V2ClientUserDto, - }) - @ApiResponse({ - status: HttpStatus.CONFLICT, - description: "Такой пользователь уже существует", - }) - @ResultDto(V2ClientUserDto) - @HttpCode(HttpStatus.CREATED) - @Post("sign-up") - async signUp(@Body() reqDto: SignUpDto): Promise { - if ( - !(await this.scheduleService.getGroupNames()).names.includes( - reqDto.group.replaceAll(" ", ""), - ) - ) { - throw new NotFoundException( - "Передано название несуществующей группы", - ); - } - - return V2ClientUserDto.fromUser(await this.authService.signUp(reqDto)); - } - - @ApiOperation({ summary: "Обновление просроченного токена" }) - @ApiBody({ type: UpdateTokenDto }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Токен обновлён успешно", - type: V2ClientUserDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: "Передан несуществующий или недействительный токен", - }) - @ResultDto(V2ClientUserDto) - @HttpCode(HttpStatus.OK) - @Post("update-token") - async updateToken( - @Body() reqDto: UpdateTokenDto, - ): Promise { - return V2ClientUserDto.fromUser( - await this.authService.updateToken(reqDto.accessToken), - ); - } -} diff --git a/src/contants.ts b/src/contants.ts index 00f5831..bab9a0b 100644 --- a/src/contants.ts +++ b/src/contants.ts @@ -4,23 +4,23 @@ import * as process from "node:process"; configDotenv(); export const jwtConstants = { - secret: process.env.JWT_SECRET!, + secret: process.env.JWT_SECRET, }; export const httpsConstants = { - certPath: process.env.CERT_PEM_PATH!, - keyPath: process.env.KEY_PEM_PATH!, + certPath: process.env.CERT_PEM_PATH, + keyPath: process.env.KEY_PEM_PATH, }; export const apiConstants = { port: +(process.env.API_PORT ?? 5050), - version: process.env.SERVER_VERSION!, + version: process.env.SERVER_VERSION, }; export const firebaseConstants = { - serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!, + serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH, }; export const scheduleConstants = { - cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY! ?? 5), + cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY ?? 5), }; diff --git a/src/firebase-admin/firebase-admin.controller.ts b/src/firebase-admin/firebase-admin.controller.ts index 5837f9e..330e2de 100644 --- a/src/firebase-admin/firebase-admin.controller.ts +++ b/src/firebase-admin/firebase-admin.controller.ts @@ -15,7 +15,7 @@ import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { FirebaseAdminService } from "./firebase-admin.service"; import { FcmPostUpdateDto } from "./dto/fcm-post-update.dto"; import { isSemVer } from "class-validator"; -import { User } from "../users/entity/user.entity"; +import User from "../users/entity/user.entity"; import { ApiBearerAuth, ApiBody, @@ -24,7 +24,7 @@ import { ApiTags, } from "@nestjs/swagger"; import { AuthRoles } from "../auth/auth-role.decorator"; -import { UserRole } from "../users/user-role.enum"; +import UserRole from "../users/user-role.enum"; import { TokenMessage, TopicMessage, diff --git a/src/firebase-admin/firebase-admin.service.ts b/src/firebase-admin/firebase-admin.service.ts index f7f0acf..9a41ff5 100644 --- a/src/firebase-admin/firebase-admin.service.ts +++ b/src/firebase-admin/firebase-admin.service.ts @@ -12,9 +12,9 @@ import { import { firebaseConstants } from "../contants"; import { UsersService } from "../users/users.service"; -import { User } from "../users/entity/user.entity"; +import User from "../users/entity/user.entity"; import { TokenMessage } from "firebase-admin/lib/messaging/messaging-api"; -import { FcmUser } from "../users/entity/fcm-user.entity"; +import FCM from "../users/entity/fcm-user.entity"; import { plainToInstance } from "class-transformer"; @Injectable() @@ -47,12 +47,12 @@ export class FirebaseAdminService implements OnModuleInit { await this.messaging.send(message); } - private getFcmOrDefault(user: User, token: string): FcmUser { + private getFcmOrDefault(user: User, token: string): FCM { if (!user.fcm) { - return plainToInstance(FcmUser, { + return plainToInstance(FCM, { token: token, topics: [], - } as FcmUser); + } as FCM); } return user.fcm; @@ -68,7 +68,7 @@ export class FirebaseAdminService implements OnModuleInit { if (!isNew) { if (fcm.token === token) return { userDto: user, isNew: false }; - for (const topic in fcm.topics) + for (const topic of fcm.topics) await this.messaging.subscribeToTopic(token, topic); fcm.token = token; } diff --git a/src/main.ts b/src/main.ts index f19de7b..cc31950 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,22 +3,30 @@ import { AppModule } from "./app.module"; import { ValidatorOptions } from "class-validator"; import { PartialValidationPipe } from "./utility/validation/partial-validation.pipe"; import { ClassValidatorInterceptor } from "./utility/validation/class-validator.interceptor"; -import { RedocModule } from "nest-redoc"; import { apiConstants, httpsConstants } from "./contants"; import * as fs from "node:fs"; import { VersioningType } from "@nestjs/common"; +import { + FastifyAdapter, + NestFastifyApplication, +} from "@nestjs/platform-fastify"; +import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; async function bootstrap() { - const app = await NestFactory.create(AppModule, { - httpsOptions: { - cert: fs.readFileSync(httpsConstants.certPath), - key: fs.readFileSync(httpsConstants.keyPath), + const app = await NestFactory.create( + AppModule, + new FastifyAdapter(), + { + httpsOptions: { + cert: fs.readFileSync(httpsConstants.certPath), + key: fs.readFileSync(httpsConstants.keyPath), + }, }, - }); + ); const validatorOptions: ValidatorOptions = { enableDebugMessages: true, forbidNonWhitelisted: true, - whitelist: true, + strictGroups: true, }; app.useGlobalPipes(new PartialValidationPipe(validatorOptions)); app.useGlobalInterceptors(new ClassValidatorInterceptor(validatorOptions)); @@ -29,21 +37,24 @@ async function bootstrap() { type: VersioningType.URI, }); - const swaggerConfig = RedocModule.createDocumentBuilder() + const swaggerConfig = new DocumentBuilder() .setTitle("Schedule Parser") .setDescription("Парсер расписания") .setVersion(apiConstants.version) .build(); - const swaggerDocument = RedocModule.createDocument(app, swaggerConfig, { + + const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig, { deepScanRoutes: true, }); + swaggerDocument.servers = [ { url: `https://localhost:${apiConstants.port}`, description: "Локальный сервер для разработки", }, ]; - await RedocModule.setup("api-docs", app, swaggerDocument, {}); + + SwaggerModule.setup("api-docs", app, swaggerDocument, {}); await app.listen(apiConstants.port); } diff --git a/src/schedule/dto/cache-status.dto.ts b/src/schedule/dto/cache-status.dto.ts index 6af2fa6..d81cd23 100644 --- a/src/schedule/dto/cache-status.dto.ts +++ b/src/schedule/dto/cache-status.dto.ts @@ -1,6 +1,6 @@ import { IsBoolean, IsHash, IsNumber } from "class-validator"; -export class CacheStatusDto { +export default class CacheStatusDto { /** * Хеш данных парсера * @example "40bd001563085fc35165329ea1ff5c5ecbdbbeef" diff --git a/src/schedule/dto/clear-schedule-replacer.dto.ts b/src/schedule/dto/clear-replacer.dto.ts similarity index 82% rename from src/schedule/dto/clear-schedule-replacer.dto.ts rename to src/schedule/dto/clear-replacer.dto.ts index ddb4e32..cd191cd 100644 --- a/src/schedule/dto/clear-schedule-replacer.dto.ts +++ b/src/schedule/dto/clear-replacer.dto.ts @@ -1,6 +1,6 @@ import { IsNumber } from "class-validator"; -export class ClearScheduleReplacerDto { +export default class ClearReplacerDto { /** * Количество удалённых заменителей расписания * @example 1 diff --git a/src/schedule/dto/schedule-group-names.dto.ts b/src/schedule/dto/get-group-names.dto.ts similarity index 79% rename from src/schedule/dto/schedule-group-names.dto.ts rename to src/schedule/dto/get-group-names.dto.ts index b8d33ee..5ac6467 100644 --- a/src/schedule/dto/schedule-group-names.dto.ts +++ b/src/schedule/dto/get-group-names.dto.ts @@ -1,6 +1,6 @@ import { IsArray } from "class-validator"; -export class ScheduleGroupNamesDto { +export default class GetGroupNamesDto { /** * Группы * @example ["ИС-214/23", "ИС-213/23"] diff --git a/src/schedule/dto/lesson.dto.ts b/src/schedule/dto/lesson.dto.ts deleted file mode 100644 index 450a89a..0000000 --- a/src/schedule/dto/lesson.dto.ts +++ /dev/null @@ -1,112 +0,0 @@ -import "reflect-metadata"; - -import { V2LessonType } from "../enum/v2-lesson-type.enum"; -import { - IsArray, - IsEnum, - IsOptional, - IsString, - ValidateNested, -} from "class-validator"; -import { Transform, Type } from "class-transformer"; -import { NullIf } from "../../utility/class-validators/conditional-field"; -import { LessonTimeDto } from "./lesson-time.dto"; -import { LessonSubGroupDto } from "./lesson-sub-group.dto"; - -export class LessonDto { - /** - * Тип занятия - * @example DEFAULT - */ - @IsEnum(V2LessonType) - @Transform(({ value, options }) => { - if (options?.groups?.includes("v1")) { - switch (value as V2LessonType) { - case V2LessonType.CONSULTATION: - case V2LessonType.INDEPENDENT_WORK: - case V2LessonType.EXAM: - case V2LessonType.EXAM_WITH_GRADE: - case V2LessonType.EXAM_DEFAULT: - return V2LessonType.DEFAULT; - default: - return value; - } - } else if (options?.groups?.includes("v2")) { - switch (value as V2LessonType) { - case V2LessonType.EXAM_DEFAULT: - return V2LessonType.EXAM; - default: - return value; - } - } - - return value; - }) - type: V2LessonType; - - /** - * Индексы пар, если присутствуют - * @example [1, 3] - * @optional - */ - @IsArray() - @ValidateNested({ each: true }) - @Type(() => Number) - @IsOptional() - @NullIf((self: LessonDto) => { - return self.type !== V2LessonType.DEFAULT; - }) - defaultRange: Array | null; - - /** - * Название занятия - * @example "Элементы высшей математики" - * @optional - */ - @IsString() - @IsOptional() - @NullIf((self: LessonDto) => { - return self.type === V2LessonType.BREAK; - }) - @Transform(({ value, obj, options }) => { - if (!value) return value; - - if (options?.groups?.includes("v1")) { - switch (obj.type as V2LessonType) { - case V2LessonType.INDEPENDENT_WORK: - return `Самостоятельная | ${value}`; - case V2LessonType.CONSULTATION: - return `Консультация | ${value}`; - case V2LessonType.EXAM: - return `ЗАЧЕТ | ${value}`; - case V2LessonType.EXAM_WITH_GRADE: - return `ЗАЧЕТ С ОЦЕНКОЙ | ${value}`; - case V2LessonType.EXAM_DEFAULT: - return `ЗАЧЕТ С ОЦЕНКОЙ | ${value}`; - default: - return value; - } - } - - return value; - }) - name: string | null; - - /** - * Начало и конец занятия - */ - @Type(() => LessonTimeDto) - time: LessonTimeDto; - - /** - * Тип занятия - */ - @IsArray() - @ValidateNested({ each: true }) - @Type(() => LessonSubGroupDto) - @IsOptional() - @NullIf((self: LessonDto) => { - return self.type !== V2LessonType.DEFAULT; - }) - subGroups: Array | null; -} diff --git a/src/schedule/dto/schedule-replacer.dto.ts b/src/schedule/dto/replacer.dto.ts similarity index 61% rename from src/schedule/dto/schedule-replacer.dto.ts rename to src/schedule/dto/replacer.dto.ts index 06377ad..e223579 100644 --- a/src/schedule/dto/schedule-replacer.dto.ts +++ b/src/schedule/dto/replacer.dto.ts @@ -1,8 +1,8 @@ import { PickType } from "@nestjs/swagger"; import { IsNumber } from "class-validator"; -import { SetScheduleReplacerDto } from "./set-schedule-replacer.dto"; +import SetScheduleReplacerDto from "./set-schedule-replacer.dto"; -export class ScheduleReplacerDto extends PickType(SetScheduleReplacerDto, [ +export default class ReplacerDto extends PickType(SetScheduleReplacerDto, [ "etag", ]) { /** diff --git a/src/schedule/dto/set-schedule-replacer.dto.ts b/src/schedule/dto/set-schedule-replacer.dto.ts index b6b044b..5841de5 100644 --- a/src/schedule/dto/set-schedule-replacer.dto.ts +++ b/src/schedule/dto/set-schedule-replacer.dto.ts @@ -1,6 +1,6 @@ import { IsMongoId, IsObject, IsString } from "class-validator"; -export class SetScheduleReplacerDto { +export default class SetScheduleReplacerDto { /** * Идентификатор заменителя (ObjectId) * @example "66e6f1c8775ffeda400d7967" diff --git a/src/schedule/dto/teacher-day.dto.ts b/src/schedule/dto/teacher-day.dto.ts deleted file mode 100644 index 24f751f..0000000 --- a/src/schedule/dto/teacher-day.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DayDto } from "./day.dto"; -import { IsArray, ValidateNested } from "class-validator"; -import { Type } from "class-transformer"; -import { OmitType } from "@nestjs/swagger"; -import { TeacherLessonDto } from "./teacher-lesson.dto"; - -export class TeacherDayDto extends OmitType(DayDto, ["lessons"]) { - /** - * Занятия - */ - @IsArray() - @ValidateNested({ each: true }) - @Type(() => TeacherLessonDto) - lessons: Array; -} diff --git a/src/schedule/dto/schedule-teacher-names.dto.ts b/src/schedule/dto/teacher-names.dto.ts similarity index 79% rename from src/schedule/dto/schedule-teacher-names.dto.ts rename to src/schedule/dto/teacher-names.dto.ts index f663a21..dfaeddf 100644 --- a/src/schedule/dto/schedule-teacher-names.dto.ts +++ b/src/schedule/dto/teacher-names.dto.ts @@ -1,6 +1,6 @@ import { IsArray } from "class-validator"; -export class ScheduleTeacherNamesDto { +export default class TeacherNamesDto { /** * Группы * @example ["Хомченко Н.Е."] diff --git a/src/schedule/dto/update-download-url.dto.ts b/src/schedule/dto/update-download-url.dto.ts index d0b3797..295d83d 100644 --- a/src/schedule/dto/update-download-url.dto.ts +++ b/src/schedule/dto/update-download-url.dto.ts @@ -1,6 +1,6 @@ import { IsUrl } from "class-validator"; -export class UpdateDownloadUrlDto { +export default class UpdateDownloadUrlDto { /** * Прямая ссылка на скачивание расписания * @example "https://politehnikum-eng.ru/2024/poltavskaja_07_s_14_po_20_10-5-.xls" diff --git a/src/schedule/dto/day.dto.ts b/src/schedule/entities/day.entity.ts similarity index 51% rename from src/schedule/dto/day.dto.ts rename to src/schedule/entities/day.entity.ts index 905e44c..7c5e154 100644 --- a/src/schedule/dto/day.dto.ts +++ b/src/schedule/entities/day.entity.ts @@ -5,21 +5,16 @@ import { IsString, ValidateNested, } from "class-validator"; -import { Transform, Type } from "class-transformer"; -import { LessonDto } from "./lesson.dto"; +import { Type } from "class-transformer"; +import Lesson from "./lesson.entity"; -export class DayDto { +// noinspection JSClassNamingConvention +export default class Day { /** * День недели * @example "Понедельник" */ @IsString() - @Transform(({ value, obj, options }) => { - if ((obj as DayDto).street && options?.groups?.includes("v1")) - return `${value} | ${(obj as DayDto).street}`; - - return value; - }) name: string; /** @@ -27,11 +22,6 @@ export class DayDto { * @example "Железнодорожная, 13" */ @IsString() - @Transform(({ value, options }) => { - if (value && options?.groups?.includes("v1")) return undefined; - - return value; - }) @IsOptional() street?: string; @@ -47,6 +37,6 @@ export class DayDto { */ @IsArray() @ValidateNested({ each: true }) - @Type(() => LessonDto) - lessons: Array; + @Type(() => Lesson) + lessons: Array; } diff --git a/src/schedule/dto/group-schedule.dto.ts b/src/schedule/entities/group-schedule.entity.ts similarity index 68% rename from src/schedule/dto/group-schedule.dto.ts rename to src/schedule/entities/group-schedule.entity.ts index 2a62e51..2dae2a4 100644 --- a/src/schedule/dto/group-schedule.dto.ts +++ b/src/schedule/entities/group-schedule.entity.ts @@ -1,16 +1,16 @@ import { PickType } from "@nestjs/swagger"; import { IsArray, IsObject, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; -import { ScheduleDto } from "./schedule.dto"; -import { GroupDto } from "./group.dto"; +import Schedule from "./schedule.entity"; +import Group from "./group.entity"; -export class GroupScheduleDto extends PickType(ScheduleDto, ["updatedAt"]) { +export default class GroupSchedule extends PickType(Schedule, ["updatedAt"]) { /** * Расписание группы */ @IsObject() - @Type(() => GroupDto) - group: GroupDto; + @Type(() => Group) + group: Group; /** * Обновлённые дни с последнего изменения расписания diff --git a/src/schedule/dto/group.dto.ts b/src/schedule/entities/group.entity.ts similarity index 75% rename from src/schedule/dto/group.dto.ts rename to src/schedule/entities/group.entity.ts index 1c3abd4..8d88e7d 100644 --- a/src/schedule/dto/group.dto.ts +++ b/src/schedule/entities/group.entity.ts @@ -1,8 +1,8 @@ import { IsArray, IsString, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; -import { DayDto } from "./day.dto"; +import Day from "./day.entity"; -export class GroupDto { +export default class Group { /** * Название группы * @example "ИС-214/23" @@ -15,6 +15,6 @@ export class GroupDto { */ @IsArray() @ValidateNested({ each: true }) - @Type(() => DayDto) - days: Array; + @Type(() => Day) + days: Array; } diff --git a/src/schedule/dto/lesson-sub-group.dto.ts b/src/schedule/entities/lesson-sub-group.entity.ts similarity index 67% rename from src/schedule/dto/lesson-sub-group.dto.ts rename to src/schedule/entities/lesson-sub-group.entity.ts index eee797c..cda037d 100644 --- a/src/schedule/dto/lesson-sub-group.dto.ts +++ b/src/schedule/entities/lesson-sub-group.entity.ts @@ -1,6 +1,11 @@ import { IsNumber, IsOptional, IsString } from "class-validator"; +import { + ClassTransformerCtor, + Ctor, +} from "../../utility/class-trasformer/class-transformer-ctor"; -export class LessonSubGroupDto { +@ClassTransformerCtor() +export default class LessonSubGroup extends Ctor { /** * Номер подгруппы * @example 1 diff --git a/src/schedule/dto/lesson-time.dto.ts b/src/schedule/entities/lesson-time.entity.ts similarity index 59% rename from src/schedule/dto/lesson-time.dto.ts rename to src/schedule/entities/lesson-time.entity.ts index e0c3ada..52ec8f3 100644 --- a/src/schedule/dto/lesson-time.dto.ts +++ b/src/schedule/entities/lesson-time.entity.ts @@ -1,6 +1,11 @@ import { IsDateString } from "class-validator"; +import { + ClassTransformerCtor, + Ctor, +} from "../../utility/class-trasformer/class-transformer-ctor"; -export class LessonTimeDto { +@ClassTransformerCtor() +export default class LessonTime extends Ctor { /** * Начало занятия * @example "2024-10-07T04:30:00.000Z" diff --git a/src/schedule/entities/lesson.entity.ts b/src/schedule/entities/lesson.entity.ts new file mode 100644 index 0000000..c57f3f9 --- /dev/null +++ b/src/schedule/entities/lesson.entity.ts @@ -0,0 +1,72 @@ +import "reflect-metadata"; + +import { LessonType } from "../enum/lesson-type.enum"; +import { + IsArray, + IsEnum, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; +import { Type } from "class-transformer"; +import { NullIf } from "../../utility/class-validators/conditional-field"; +import LessonTime from "./lesson-time.entity"; +import LessonSubGroup from "./lesson-sub-group.entity"; +import { + ClassTransformerCtor, + Ctor, +} from "../../utility/class-trasformer/class-transformer-ctor"; + +@ClassTransformerCtor() +export default class Lesson extends Ctor { + /** + * Тип занятия + * @example DEFAULT + */ + @IsEnum(LessonType) + type: LessonType; + + /** + * Индексы пар, если присутствуют + * @example [1, 3] + * @optional + */ + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Number) + @IsOptional() + @NullIf((self: Lesson) => { + return self.type !== LessonType.DEFAULT; + }) + defaultRange: Array | null; + + /** + * Название занятия + * @example "Элементы высшей математики" + * @optional + */ + @IsString() + @IsOptional() + @NullIf((self: Lesson) => { + return self.type === LessonType.BREAK; + }) + name: string | null; + + /** + * Начало и конец занятия + */ + @Type(() => LessonTime) + time: LessonTime; + + /** + * Тип занятия + */ + @IsArray() + @ValidateNested({ each: true }) + @Type(() => LessonSubGroup) + @IsOptional() + @NullIf((self: Lesson) => { + return self.type !== LessonType.DEFAULT; + }) + subGroups: Array | null; +} diff --git a/src/schedule/dto/schedule.dto.ts b/src/schedule/entities/schedule.entity.ts similarity index 83% rename from src/schedule/dto/schedule.dto.ts rename to src/schedule/entities/schedule.entity.ts index f9ff1a2..eade91c 100644 --- a/src/schedule/dto/schedule.dto.ts +++ b/src/schedule/entities/schedule.entity.ts @@ -1,9 +1,9 @@ import { IsArray, IsDate, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; -import { GroupDto } from "./group.dto"; +import Group from "./group.entity"; import { ToMap } from "create-map-transform-fn"; -export class ScheduleDto { +export default class Schedule { /** * Дата когда последний раз расписание было скачано с сервера политехникума * @example "2024-10-18T21:50:06.680Z" @@ -14,8 +14,8 @@ export class ScheduleDto { /** * Расписание групп */ - @ToMap({ mapValueClass: GroupDto }) - groups: Map; + @ToMap({ mapValueClass: Group }) + groups: Map; /** * Обновлённые дни с последнего изменения расписания diff --git a/src/schedule/entities/teacher-day.entity.ts b/src/schedule/entities/teacher-day.entity.ts new file mode 100644 index 0000000..09493bc --- /dev/null +++ b/src/schedule/entities/teacher-day.entity.ts @@ -0,0 +1,46 @@ +import { + IsArray, + IsDateString, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; +import { Type } from "class-transformer"; +import TeacherLesson from "./teacher-lesson.entity"; +import { + ClassTransformerCtor, + Ctor, +} from "../../utility/class-trasformer/class-transformer-ctor"; + +@ClassTransformerCtor() +export default class TeacherDay extends Ctor { + /** + * День недели + * @example "Понедельник" + */ + @IsString() + name: string; + + /** + * Улица (v2) + * @example "Железнодорожная, 13" + */ + @IsString() + @IsOptional() + street?: string; + + /** + * Дата + * @example "2024-10-06T20:00:00.000Z" + */ + @IsDateString() + date: Date; + + /** + * Занятия + */ + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TeacherLesson) + lessons: Array; +} diff --git a/src/schedule/dto/teacher-lesson.dto.ts b/src/schedule/entities/teacher-lesson.entity.ts similarity index 53% rename from src/schedule/dto/teacher-lesson.dto.ts rename to src/schedule/entities/teacher-lesson.entity.ts index 881d00b..15ff924 100644 --- a/src/schedule/dto/teacher-lesson.dto.ts +++ b/src/schedule/entities/teacher-lesson.entity.ts @@ -1,9 +1,9 @@ -import { LessonDto } from "./lesson.dto"; +import Lesson from "./lesson.entity"; import { IsOptional, IsString } from "class-validator"; import { NullIf } from "../../utility/class-validators/conditional-field"; -import { V2LessonType } from "../enum/v2-lesson-type.enum"; +import { LessonType } from "../enum/lesson-type.enum"; -export class TeacherLessonDto extends LessonDto { +export default class TeacherLesson extends Lesson { /** * Название группы * @example "ИС-214/23" @@ -11,8 +11,8 @@ export class TeacherLessonDto extends LessonDto { */ @IsString() @IsOptional() - @NullIf((self: TeacherLessonDto) => { - return self.type === V2LessonType.BREAK; + @NullIf((self: TeacherLesson) => { + return self.type === LessonType.BREAK; }) group: string | null; } diff --git a/src/schedule/dto/teacher-schedule.dto.ts b/src/schedule/entities/teacher-schedule.entity.ts similarity index 70% rename from src/schedule/dto/teacher-schedule.dto.ts rename to src/schedule/entities/teacher-schedule.entity.ts index 9b21f42..83f486f 100644 --- a/src/schedule/dto/teacher-schedule.dto.ts +++ b/src/schedule/entities/teacher-schedule.entity.ts @@ -1,15 +1,15 @@ import { PickType } from "@nestjs/swagger"; -import { ScheduleDto } from "./schedule.dto"; +import Schedule from "./schedule.entity"; import { IsArray, IsObject, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; -import { TeacherDto } from "./teacher.dto"; +import Teacher from "./teacher.entity"; -export class TeacherScheduleDto extends PickType(ScheduleDto, ["updatedAt"]) { +export default class TeacherSchedule extends PickType(Schedule, ["updatedAt"]) { /** * Расписание преподавателя */ @IsObject() - teacher: TeacherDto; + teacher: Teacher; /** * Обновлённые дни с последнего изменения расписания diff --git a/src/schedule/dto/teacher.dto.ts b/src/schedule/entities/teacher.entity.ts similarity index 54% rename from src/schedule/dto/teacher.dto.ts rename to src/schedule/entities/teacher.entity.ts index 418b442..1ed7627 100644 --- a/src/schedule/dto/teacher.dto.ts +++ b/src/schedule/entities/teacher.entity.ts @@ -1,8 +1,13 @@ import { IsArray, IsString, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; -import { TeacherDayDto } from "./teacher-day.dto"; +import TeacherDay from "./teacher-day.entity"; +import { + ClassTransformerCtor, + Ctor, +} from "../../utility/class-trasformer/class-transformer-ctor"; -export class TeacherDto { +@ClassTransformerCtor() +export default class Teacher extends Ctor { /** * ФИО преподавателя * @example "Хомченко Н.Е." @@ -15,6 +20,6 @@ export class TeacherDto { */ @IsArray() @ValidateNested({ each: true }) - @Type(() => TeacherDayDto) - days: Array; + @Type(() => TeacherDay) + days: Array; } diff --git a/src/schedule/enum/v2-lesson-type.enum.ts b/src/schedule/enum/lesson-type.enum.ts similarity index 91% rename from src/schedule/enum/v2-lesson-type.enum.ts rename to src/schedule/enum/lesson-type.enum.ts index 9e72291..4e705b4 100644 --- a/src/schedule/enum/v2-lesson-type.enum.ts +++ b/src/schedule/enum/lesson-type.enum.ts @@ -1,4 +1,4 @@ -export enum V2LessonType { +export enum LessonType { DEFAULT = 0, // Обычная ADDITIONAL, // Допы BREAK, // Перемена diff --git a/src/schedule/internal/schedule-parser/v2-schedule-parser.spec.ts b/src/schedule/internal/schedule-parser/schedule-parser.spec.ts similarity index 76% rename from src/schedule/internal/schedule-parser/v2-schedule-parser.spec.ts rename to src/schedule/internal/schedule-parser/schedule-parser.spec.ts index c8813ca..604d309 100644 --- a/src/schedule/internal/schedule-parser/v2-schedule-parser.spec.ts +++ b/src/schedule/internal/schedule-parser/schedule-parser.spec.ts @@ -1,15 +1,15 @@ -import { V2ScheduleParser, V2ScheduleParseResult } from "./v2-schedule-parser"; +import { ScheduleParser, ScheduleParseResult } from "./schedule-parser"; import { BasicXlsDownloader } from "../xls-downloader/basic-xls-downloader"; -import { DayDto } from "../../dto/day.dto"; -import { GroupDto } from "../../dto/group.dto"; +import Day from "../../entities/day.entity"; +import Group from "../../entities/group.entity"; import instanceToInstance2 from "../../../utility/class-trasformer/instance-to-instance-2"; -describe("V2ScheduleParser", () => { - let parser: V2ScheduleParser; +describe("ScheduleParser", () => { + let parser: ScheduleParser; - beforeEach(async () => { + beforeEach(() => { const xlsDownloader = new BasicXlsDownloader(); - parser = new V2ScheduleParser(xlsDownloader); + parser = new ScheduleParser(xlsDownloader); }); describe("Ошибки", () => { @@ -32,10 +32,10 @@ describe("V2ScheduleParser", () => { const schedule = await parser.getSchedule(); expect(schedule).toBeDefined(); - const group: GroupDto | undefined = schedule.groups.get("ИС-214/23"); + const group: Group | undefined = schedule.groups.get("ИС-214/23"); expect(group).toBeDefined(); - const monday: DayDto = group.days[0]; + const monday: Day = group.days[0]; expect(monday).toBeDefined(); const name = monday.name; @@ -63,14 +63,13 @@ describe("V2ScheduleParser", () => { it("Зачёт с оценкой v2", async () => { const schedule = await parser.getSchedule().then((v) => - instanceToInstance2(V2ScheduleParseResult, v, { + instanceToInstance2(ScheduleParseResult, v, { groups: ["v2"], }), ); expect(schedule).toBeDefined(); - const group: GroupDto | undefined = - schedule.groups.get("ИС-214/23"); + const group: Group | undefined = schedule.groups.get("ИС-214/23"); expect(group).toBeDefined(); const day = group.days[0]; diff --git a/src/schedule/internal/schedule-parser/v2-schedule-parser.ts b/src/schedule/internal/schedule-parser/schedule-parser.ts similarity index 66% rename from src/schedule/internal/schedule-parser/v2-schedule-parser.ts rename to src/schedule/internal/schedule-parser/schedule-parser.ts index 1941b22..c999539 100644 --- a/src/schedule/internal/schedule-parser/v2-schedule-parser.ts +++ b/src/schedule/internal/schedule-parser/schedule-parser.ts @@ -5,17 +5,17 @@ import { Range, WorkSheet } from "xlsx"; import { toNormalString, trimAll } from "../../../utility/string.util"; import { plainToClass, plainToInstance, Type } from "class-transformer"; import * as objectHash from "object-hash"; -import { LessonTimeDto } from "../../dto/lesson-time.dto"; -import { V2LessonType } from "../../enum/v2-lesson-type.enum"; -import { LessonSubGroupDto } from "../../dto/lesson-sub-group.dto"; -import { LessonDto } from "../../dto/lesson.dto"; -import { DayDto } from "../../dto/day.dto"; -import { GroupDto } from "../../dto/group.dto"; +import LessonTime from "../../entities/lesson-time.entity"; +import { LessonType } from "../../enum/lesson-type.enum"; +import LessonSubGroup from "../../entities/lesson-sub-group.entity"; +import Lesson from "../../entities/lesson.entity"; +import Day from "../../entities/day.entity"; +import Group from "../../entities/group.entity"; import * as assert from "node:assert"; import { ScheduleReplacerService } from "../../schedule-replacer.service"; -import { TeacherDto } from "../../dto/teacher.dto"; -import { TeacherDayDto } from "../../dto/teacher-day.dto"; -import { TeacherLessonDto } from "../../dto/teacher-lesson.dto"; +import Teacher from "../../entities/teacher.entity"; +import TeacherDay from "../../entities/teacher-day.entity"; +import TeacherLesson from "../../entities/teacher-lesson.entity"; import { IsArray, IsDate, @@ -24,6 +24,7 @@ import { ValidateNested, } from "class-validator"; import { ToMap } from "create-map-transform-fn"; +import { ClassProperties } from "../../../utility/class-trasformer/class-transformer-ctor"; type InternalId = { /** @@ -46,12 +47,12 @@ type InternalTime = { /** * Временной отрезок */ - timeRange: LessonTimeDto; + timeRange: LessonTime; /** * Тип пары на этой строке */ - lessonType: V2LessonType; + lessonType: LessonType; /** * Индекс пары на этой строке @@ -64,7 +65,7 @@ type InternalTime = { xlsxRange: Range; }; -export class V2ScheduleParseResult { +export class ScheduleParseResult { /** * ETag расписания */ @@ -94,15 +95,15 @@ export class V2ScheduleParseResult { * Расписание групп в виде списка. * Ключ - название группы. */ - @ToMap({ mapValueClass: GroupDto }) - groups: Map; + @ToMap({ mapValueClass: Group }) + groups: Map; /** * Расписание преподавателей в виде списка. * Ключ - ФИО преподавателя */ - @ToMap({ mapValueClass: TeacherDto }) - teachers: Map; + @ToMap({ mapValueClass: Teacher }) + teachers: Map; /** * Список групп у которых было обновлено расписание с момента последнего обновления файла. @@ -126,8 +127,8 @@ export class V2ScheduleParseResult { updatedTeachers: Array>; } -export class V2ScheduleParser { - private lastResult: V2ScheduleParseResult | null = null; +export class ScheduleParser { + private lastResult: ScheduleParseResult | null = null; /** * @param xlsDownloader - класс для загрузки расписания с сайта политехникума @@ -176,113 +177,94 @@ export class V2ScheduleParser { row: number, column: number, ): string | null { - const cell: XLSX.CellObject | null = - worksheet[XLSX.utils.encode_cell({ r: row, c: column })]; + const cell = worksheet[ + XLSX.utils.encode_cell({ r: row, c: column }) + ] as XLSX.CellObject; return toNormalString(cell?.w); } /** * Парсит информацию о паре исходя из текста в записи - * @param lessonName - текст в записи + * @param text - текст в записи * @returns {{ * name: string; - * subGroups: Array; + * subGroups: Array; * }} - название пары и список подгрупп * @private * @static */ - private static parseNameAndSubGroups(lessonName: string): { + private static parseNameAndSubGroups(text: string): { name: string; - subGroups: Array; + subGroups: Array; } { + if (!text) return { name: text, subGroups: [] }; + // хд + const lessonRegExp = /(?:[А-Я][а-я]+[А-Я]{2}(?:\([0-9][а-я]+\))?)+$/m; + const teacherRegExp = + /([А-Я][а-я]+)([А-Я])([А-Я])(?:\(([0-9])[а-я]+\))?/g; - const allRegex = - /(?:[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\.(?:\s?\(\s?[0-9]\s?подгруппа\s?\))?(?:,\s)?)+$/gm; - const teacherAndSubGroupRegex = - /(?:[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\.(?:\s?\(\s?[0-9]\s?подгруппа\s?\))?)+/gm; + const rawTeachers = (text + .replaceAll(/[\s\n\t.,]+/g, "") + .match(lessonRegExp) ?? [])[0]; - const allMatch = allRegex.exec(lessonName); + // если не ничего не найдено + if (!rawTeachers) + return { + name: text + .replaceAll(/[\t\n]+/g, "") // Убираем все переносы + .replaceAll(/\s+/g, " ") // Убираем все лишние пробелы + .trim() // Убираем пробелы по краям + .replace(/\.$/m, ""), // Убираем точку в конце названия, если присутствует + subGroups: [], + }; - // если не ничё не найдено - if (allMatch === null) return { name: lessonName, subGroups: [] }; + const teacherIt = rawTeachers.matchAll(teacherRegExp); - const all: Array = []; + const subGroups: Array = []; + let lessonName: string; - let allInnerMatch: RegExpExecArray; - while ( - (allInnerMatch = teacherAndSubGroupRegex.exec(allMatch[0])) !== null - ) { - if (allInnerMatch.index === teacherAndSubGroupRegex.lastIndex) - teacherAndSubGroupRegex.lastIndex++; - - all.push(allInnerMatch[0].trim()); - } - - // парадокс - if (all.length === 0) { - throw new Error("Парадокс"); - } - - const subGroups: Array = []; - - for (const teacherAndSubGroup of all) { - const teacherRegex = /[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\./g; - const subGroupRegex = /\([0-9]подгруппа\)/g; - - const teacherMatch = teacherRegex.exec(teacherAndSubGroup); - if (teacherMatch === null) throw new Error("Парадокс"); - - let teacherFIO = teacherMatch[0]; - const teacherSpaceIndex = teacherFIO.indexOf(" ") + 1; - const teacherIO = teacherFIO - .substring(teacherSpaceIndex) - .replaceAll("s", ""); - - teacherFIO = `${teacherFIO.substring(0, teacherSpaceIndex)}${teacherIO}`; - - const subGroupMatch = subGroupRegex.exec( - teacherAndSubGroup.replaceAll(" ", ""), - ); - const subGroup = subGroupMatch - ? Number.parseInt(subGroupMatch[0][1]) - : 1; + let m: RegExpMatchArray; + while ((m = teacherIt.next().value as RegExpMatchArray)) { + if (!lessonName) { + lessonName = text + .substring(0, text.indexOf(m[1])) + .replaceAll(/[\t\n]+/g, "") // Убираем все переносы + .replaceAll(/\s+/g, " ") // Убираем все лишние пробелы + .trim() // Убираем пробелы по краям + .replace(/\.$/m, ""); // Убираем точку в конце названия, если присутствует + } subGroups.push( - plainToClass(LessonSubGroupDto, { - teacher: teacherFIO, - number: subGroup, - cabinet: "", + new LessonSubGroup({ + number: +(m[4] ?? "0"), + cabinet: null, + teacher: `${m[1]} ${m[2]}.${m[3]}.`, }), ); } - for (const index in subGroups) { - if (subGroups.length === 1) { - break; - } + // фикс, если у кого-то отсутствует индекс подгруппы - // бляздец - switch (index) { - case "0": - subGroups[index].number = - subGroups[+index + 1].number === 2 ? 1 : 2; - continue; - case "1": - subGroups[index].number = - subGroups[+index - 1].number === 1 ? 2 : 1; - continue; - default: - subGroups[index].number = +index; + // если 1 преподаватель + if (subGroups.length === 1) subGroups[0].number = 1; + else if (subGroups.length === 2) { + // если индексы отсутствуют у обоих, ставим поочерёдно + if (subGroups[0].number === 0 && subGroups[1].number === 0) { + subGroups[0].number = 1; + subGroups[1].number = 2; } + // если индекс отсутствует у первого, ставим 2, если у второго индекс 1 и наоборот + else if (subGroups[0].number === 0) + subGroups[0].number = subGroups[1].number === 1 ? 2 : 1; + // если индекс отсутствует у второго, ставим 2, если у первого индекс 1 и наоборот + else if (subGroups[1].number === 0) + subGroups[1].number = subGroups[0].number === 1 ? 2 : 1; } return { - name: lessonName - .substring(0, allMatch.index) - .replaceAll(".", "") - .trim(), + name: lessonName, subGroups: subGroups, }; } @@ -308,7 +290,7 @@ export class V2ScheduleParser { const days: Array = []; for (let row = range.s.r + 1; row <= range.e.r; ++row) { - const dayName = V2ScheduleParser.getCellData(workSheet, row, 0); + const dayName = ScheduleParser.getCellData(workSheet, row, 0); if (!dayName) continue; if (!isHeaderParsed) { @@ -320,7 +302,7 @@ export class V2ScheduleParser { column <= range.e.c; ++column ) { - const groupName = V2ScheduleParser.getCellData( + const groupName = ScheduleParser.getCellData( workSheet, row, column, @@ -359,47 +341,45 @@ export class V2ScheduleParser { } private static convertGroupsToTeachers( - groups: Map, - ): Map { - const result = new Map(); + groups: Map, + ): Map { + const result = new Map(); for (const groupName of groups.keys()) { const group = groups.get(groupName); for (const day of group.days) { for (const lesson of day.lessons) { - if (lesson.type !== V2LessonType.DEFAULT) continue; + if (lesson.type !== LessonType.DEFAULT) continue; for (const subGroup of lesson.subGroups) { - let teacherDto: TeacherDto = result.get( - subGroup.teacher, - ); + let teacherDto: Teacher = result.get(subGroup.teacher); if (!teacherDto) { - teacherDto = new TeacherDto(); - result.set(subGroup.teacher, teacherDto); + teacherDto = new Teacher({ + name: subGroup.teacher, + days: [], + }); - teacherDto.name = subGroup.teacher; - teacherDto.days = []; + result.set(subGroup.teacher, teacherDto); } - let teacherDay: TeacherDayDto = - teacherDto.days[day.name]; + let teacherDay = teacherDto.days[ + day.name + ] as TeacherDay; if (!teacherDay) { teacherDay = teacherDto.days[day.name] = - new TeacherDayDto(); - - // TODO: Что это блять такое? - // noinspection JSConstantReassignment - teacherDay.name = day.name; - teacherDay.date = day.date; - teacherDay.lessons = []; + new TeacherDay({ + name: day.name, + date: day.date, + lessons: [], + }); } const teacherLesson = structuredClone( lesson, - ) as TeacherLessonDto; + ) as TeacherLesson; teacherLesson.group = groupName; teacherDay.lessons.push(teacherLesson); @@ -413,8 +393,11 @@ export class V2ScheduleParser { const days = teacher.days; + // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const dayName in days) { const day = days[dayName]; + + // eslint-disable-next-line @typescript-eslint/no-array-delete delete days[dayName]; day.lessons.sort( @@ -432,10 +415,10 @@ export class V2ScheduleParser { /** * Возвращает текущее расписание - * @returns {V2ScheduleParseResult} - расписание + * @returns {ScheduleParseResult} - расписание * @async */ - async getSchedule(): Promise { + async getSchedule(): Promise { const headData = await this.xlsDownloader.fetch(true); this.xlsDownloader.verifyFetchResult(headData); @@ -467,9 +450,9 @@ export class V2ScheduleParser { const workSheet = workBook.Sheets[workBook.SheetNames[0]]; const { groupSkeletons, daySkeletons } = - V2ScheduleParser.parseSkeleton(workSheet); + ScheduleParser.parseSkeleton(workSheet); - const groups = new Map(); + const groups = new Map(); const daysTimes: Array> = []; let daysTimesFilled = false; @@ -478,13 +461,13 @@ export class V2ScheduleParser { .e.r; for (const groupSkeleton of groupSkeletons) { - const group = new GroupDto(); + const group = new Group(); group.name = groupSkeleton.name; group.days = []; for (let dayIdx = 0; dayIdx < daySkeletons.length; ++dayIdx) { const daySkeleton = daySkeletons[dayIdx]; - const day = new DayDto(); + const day = new Day(); { const daySpaceIndex = daySkeleton.name.indexOf(" "); day.name = daySkeleton.name.substring(0, daySpaceIndex); @@ -504,8 +487,8 @@ export class V2ScheduleParser { ? daySkeletons[dayIdx + 1].row : saturdayEndRow) - daySkeleton.row; - const dayTimes: Array = daysTimesFilled - ? daysTimes[day.name] + const dayTimes = daysTimesFilled + ? (daysTimes[day.name] as Array) : []; if (!daysTimesFilled) { @@ -514,7 +497,7 @@ export class V2ScheduleParser { row < daySkeleton.row + rowDistance; ++row ) { - const time = V2ScheduleParser.getCellData( + const time = ScheduleParser.getCellData( workSheet, row, lessonTimeColumn, @@ -524,19 +507,17 @@ export class V2ScheduleParser { // type const lessonType = time.includes("пара") - ? V2LessonType.DEFAULT - : V2LessonType.ADDITIONAL; + ? LessonType.DEFAULT + : LessonType.ADDITIONAL; const defaultIndex = - lessonType === V2LessonType.DEFAULT - ? +time[0] - : null; + lessonType === LessonType.DEFAULT ? +time[0] : null; // time - const timeRange = new LessonTimeDto(); - - timeRange.start = new Date(day.date); - timeRange.end = new Date(day.date); + const timeRange = new LessonTime({ + start: new Date(day.date), + end: new Date(day.date), + }); const timeString = time.replaceAll(".", ":"); const timeRegex = /(\d+:\d+)-(\d+:\d+)/g; @@ -562,7 +543,7 @@ export class V2ScheduleParser { lessonType: lessonType, defaultIndex: defaultIndex, - xlsxRange: V2ScheduleParser.getMergeFromStart( + xlsxRange: ScheduleParser.getMergeFromStart( workSheet, row, lessonTimeColumn, @@ -574,7 +555,7 @@ export class V2ScheduleParser { } for (const time of dayTimes) { - const lessonsOrStreet = V2ScheduleParser.parseLesson( + const lessonsOrStreet = ScheduleParser.parseLesson( workSheet, day, dayTimes, @@ -583,11 +564,11 @@ export class V2ScheduleParser { ); if (typeof lessonsOrStreet === "string") { - day.street = lessonsOrStreet as string; + day.street = lessonsOrStreet; continue; } - for (const lesson of lessonsOrStreet as Array) + for (const lesson of lessonsOrStreet) day.lessons.push(lesson); } @@ -599,12 +580,12 @@ export class V2ScheduleParser { groups.set(group.name, group); } - const updatedGroups = V2ScheduleParser.getUpdatedGroups( + const updatedGroups = ScheduleParser.getUpdatedGroups( this.lastResult?.groups, groups, ); - const teachers = V2ScheduleParser.convertGroupsToTeachers(groups); + const teachers = ScheduleParser.convertGroupsToTeachers(groups); return (this.lastResult = { downloadedAt: headData.requestedAt, @@ -630,16 +611,16 @@ export class V2ScheduleParser { private static parseLesson( workSheet: XLSX.Sheet, - day: DayDto, + day: Day, dayTimes: Array, time: InternalTime, column: number, - ): Array | string { + ): Array | string { const row = time.xlsxRange.s.r; // name let rawName = trimAll( - V2ScheduleParser.getCellData(workSheet, row, column)?.replaceAll( + ScheduleParser.getCellData(workSheet, row, column)?.replaceAll( /[\n\r]/g, " ", ) ?? "", @@ -647,87 +628,94 @@ export class V2ScheduleParser { if (rawName.length === 0) return []; - const lesson = new LessonDto(); + const lessonData = {} as ClassProperties; if (this.otherStreetRegExp.test(rawName)) return rawName; else if (rawName.includes("ЭКЗАМЕН")) { - lesson.type = V2LessonType.EXAM_DEFAULT; + lessonData.type = LessonType.EXAM_DEFAULT; rawName = trimAll(rawName.replace("ЭКЗАМЕН", "")); } else if (rawName.includes("ЗАЧЕТ С ОЦЕНКОЙ")) { - lesson.type = V2LessonType.EXAM_WITH_GRADE; + lessonData.type = LessonType.EXAM_WITH_GRADE; rawName = trimAll(rawName.replace("ЗАЧЕТ С ОЦЕНКОЙ", "")); } else if (rawName.includes("ЗАЧЕТ")) { - lesson.type = V2LessonType.EXAM; + lessonData.type = LessonType.EXAM; rawName = trimAll(rawName.replace("ЗАЧЕТ", "")); } else if (rawName.includes("(консультация)")) { - lesson.type = V2LessonType.CONSULTATION; + lessonData.type = LessonType.CONSULTATION; rawName = trimAll(rawName.replace("(консультация)", "")); } else if (this.consultationRegExp.test(rawName)) { - lesson.type = V2LessonType.CONSULTATION; + lessonData.type = LessonType.CONSULTATION; rawName = trimAll(rawName.replace(this.consultationRegExp, "")); } else if (rawName.includes("САМОСТОЯТЕЛЬНАЯ РАБОТА")) { - lesson.type = V2LessonType.INDEPENDENT_WORK; + lessonData.type = LessonType.INDEPENDENT_WORK; rawName = trimAll(rawName.replace("САМОСТОЯТЕЛЬНАЯ РАБОТА", "")); - } else lesson.type = time.lessonType; + } else lessonData.type = time.lessonType; - lesson.defaultRange = + lessonData.defaultRange = time.defaultIndex !== null ? [time.defaultIndex, time.defaultIndex] : null; - lesson.time = new LessonTimeDto(); - lesson.time.start = time.timeRange.start; - // check if multi-lesson const range = this.getMergeFromStart(workSheet, row, column); const endTime = dayTimes.filter((dayTime) => { return dayTime.xlsxRange.e.r === range.e.r; })[0]; - lesson.time.end = endTime?.timeRange.end ?? time.timeRange.end; - if (lesson.defaultRange !== null) - lesson.defaultRange[1] = endTime?.defaultIndex ?? time.defaultIndex; + lessonData.time = new LessonTime({ + start: time.timeRange.start, + end: endTime?.timeRange.end ?? time.timeRange.end, + }); + + if (lessonData.defaultRange !== null) + lessonData.defaultRange[1] = + endTime?.defaultIndex ?? time.defaultIndex; // name and subGroups (subGroups unfilled) { - const nameAndGroups = V2ScheduleParser.parseNameAndSubGroups( - trimAll(rawName?.replaceAll(/[\n\r]/g, "") ?? ""), + const nameAndGroups = ScheduleParser.parseNameAndSubGroups( + rawName ?? "", ); - lesson.name = nameAndGroups.name; - lesson.subGroups = nameAndGroups.subGroups; + lessonData.name = nameAndGroups.name; + lessonData.subGroups = nameAndGroups.subGroups; } // cabinets { - const cabinets = V2ScheduleParser.parseCabinets( + const cabinets = ScheduleParser.parseCabinets( workSheet, row, column + 1, ); if (cabinets.length === 1) { - for (const index in lesson.subGroups) - lesson.subGroups[index].cabinet = cabinets[0]; - } else if (cabinets.length === lesson.subGroups.length) { - for (const index in lesson.subGroups) - lesson.subGroups[index].cabinet = cabinets[index]; + // eslint-disable-next-line @typescript-eslint/no-for-in-array + for (const index in lessonData.subGroups) + lessonData.subGroups[index].cabinet = cabinets[0]; + } else if (cabinets.length === lessonData.subGroups.length) { + // eslint-disable-next-line @typescript-eslint/no-for-in-array + for (const index in lessonData.subGroups) { + lessonData.subGroups[index].cabinet = + cabinets[lessonData.subGroups[index].number - 1]; + } } else if (cabinets.length !== 0) { - if (cabinets.length > lesson.subGroups.length) { + if (cabinets.length > lessonData.subGroups.length) { + // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const index in cabinets) { - if (lesson.subGroups[index] === undefined) { - lesson.subGroups.push( - plainToInstance(LessonSubGroupDto, { + if (lessonData.subGroups[index] === undefined) { + lessonData.subGroups.push( + plainToInstance(LessonSubGroup, { number: +index + 1, teacher: "Ошибка в расписании", cabinet: cabinets[index], - } as LessonSubGroupDto), + } as LessonSubGroup), ); continue; } - lesson.subGroups[index].cabinet = cabinets[index]; + lessonData.subGroups[index].cabinet = cabinets[index]; } } else throw new Error("Разное кол-во кабинетов и подгрупп!"); } @@ -738,20 +726,20 @@ export class V2ScheduleParser { ? null : day.lessons[day.lessons.length - 1]; - if (!prevLesson) return [lesson]; + if (!prevLesson) return [lessonData]; return [ - plainToInstance(LessonDto, { - type: V2LessonType.BREAK, + new Lesson({ + type: LessonType.BREAK, defaultRange: null, name: null, - time: plainToInstance(LessonTimeDto, { + time: new LessonTime({ start: prevLesson.time.end, - end: lesson.time.start, - } as LessonTimeDto), + end: lessonData.time.start, + }), subGroups: [], - } as LessonDto), - lesson, + }), + new Lesson(lessonData), ]; } @@ -762,7 +750,7 @@ export class V2ScheduleParser { ) { const cabinets: Array = []; { - const rawCabinets = V2ScheduleParser.getCellData( + const rawCabinets = ScheduleParser.getCellData( workSheet, row, column, @@ -782,8 +770,8 @@ export class V2ScheduleParser { } private static getUpdatedGroups( - cachedGroups: Map | null, - currentGroups: Map, + cachedGroups: Map | null, + currentGroups: Map, ): Array> { if (!cachedGroups) return []; @@ -795,6 +783,7 @@ export class V2ScheduleParser { const affectedGroupDays: Array = []; + // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const dayIdx in currentGroup.days) { if ( objectHash.sha1(currentGroup.days[dayIdx]) !== diff --git a/src/schedule/internal/xls-downloader/basic-xls-downloader.spec.ts b/src/schedule/internal/xls-downloader/basic-xls-downloader.spec.ts index 933319c..55781b8 100644 --- a/src/schedule/internal/xls-downloader/basic-xls-downloader.spec.ts +++ b/src/schedule/internal/xls-downloader/basic-xls-downloader.spec.ts @@ -4,7 +4,7 @@ import { XlsDownloaderInterface } from "./xls-downloader.interface"; describe("BasicXlsDownloader", () => { let downloader: XlsDownloaderInterface; - beforeEach(async () => { + beforeEach(() => { downloader = new BasicXlsDownloader(); }); diff --git a/src/schedule/internal/xls-downloader/basic-xls-downloader.ts b/src/schedule/internal/xls-downloader/basic-xls-downloader.ts index df67d9d..bb69d3f 100644 --- a/src/schedule/internal/xls-downloader/basic-xls-downloader.ts +++ b/src/schedule/internal/xls-downloader/basic-xls-downloader.ts @@ -52,10 +52,10 @@ export class BasicXlsDownloader implements XlsDownloaderInterface { type HeaderValue = string | undefined; - const contentType: HeaderValue = response.headers["content-type"]; - const etag: HeaderValue = response.headers["etag"]; - const uploadedAt: HeaderValue = response.headers["last-modified"]; - const requestedAt: HeaderValue = response.headers["date"]; + const contentType = response.headers["content-type"] as HeaderValue; + const etag = response.headers["etag"] as HeaderValue; + const uploadedAt = response.headers["last-modified"] as HeaderValue; + const requestedAt = response.headers["date"] as HeaderValue; if (!contentType || !etag || !uploadedAt || !requestedAt) { return { @@ -77,7 +77,7 @@ export class BasicXlsDownloader implements XlsDownloaderInterface { etag: etag, uploadedAt: new Date(uploadedAt), requestedAt: new Date(requestedAt), - data: head ? undefined : response.data.buffer, + data: head ? undefined : (response.data as Buffer).buffer, }; } diff --git a/src/schedule/schedule-replacer.controller.ts b/src/schedule/schedule-replacer.controller.ts index 4e35c2b..0f520e9 100644 --- a/src/schedule/schedule-replacer.controller.ts +++ b/src/schedule/schedule-replacer.controller.ts @@ -20,11 +20,11 @@ import { ApiTags, } from "@nestjs/swagger"; import { ResultDto } from "src/utility/validation/class-validator.interceptor"; -import { UserRole } from "../users/user-role.enum"; -import { ScheduleReplacerDto } from "./dto/schedule-replacer.dto"; -import { ClearScheduleReplacerDto } from "./dto/clear-schedule-replacer.dto"; import { plainToInstance } from "class-transformer"; import { ScheduleService } from "./schedule.service"; +import UserRole from "../users/user-role.enum"; +import ReplacerDto from "./dto/replacer.dto"; +import ClearReplacerDto from "./dto/clear-replacer.dto"; @ApiTags("v1/schedule-replacer") @ApiBearerAuth() @@ -69,13 +69,13 @@ export class ScheduleReplacerController { @HttpCode(HttpStatus.OK) @AuthRoles([UserRole.ADMIN]) @ResultDto(null) // TODO: Как нибудь сделать проверку в таких случаях - async getReplacers(): Promise { + async getReplacers(): Promise { return await this.scheduleReplaceService.getAll().then((result) => { return result.map((replacer) => { - return plainToInstance(ScheduleReplacerDto, { + return plainToInstance(ReplacerDto, { etag: replacer.etag, size: replacer.data.byteLength, - } as ScheduleReplacerDto); + } as ReplacerDto); }); }); } @@ -84,13 +84,13 @@ export class ScheduleReplacerController { @ApiResponse({ status: HttpStatus.OK, description: "Отчистка прошла успешно", - type: ClearScheduleReplacerDto, + type: ClearReplacerDto, }) @Post("clear") @HttpCode(HttpStatus.OK) @AuthRoles([UserRole.ADMIN]) - @ResultDto(ClearScheduleReplacerDto) - async clear(): Promise { + @ResultDto(ClearReplacerDto) + async clear(): Promise { const response = { count: await this.scheduleReplaceService.clear() }; await this.scheduleService.refreshCache(); diff --git a/src/schedule/schedule-replacer.service.ts b/src/schedule/schedule-replacer.service.ts index ba6e3a5..fe7340a 100644 --- a/src/schedule/schedule-replacer.service.ts +++ b/src/schedule/schedule-replacer.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; -import { SetScheduleReplacerDto } from "./dto/set-schedule-replacer.dto"; +import SetScheduleReplacerDto from "./dto/set-schedule-replacer.dto"; import { plainToInstance } from "class-transformer"; @Injectable() diff --git a/src/schedule/v2-schedule.controller.ts b/src/schedule/schedule.controller.ts similarity index 63% rename from src/schedule/v2-schedule.controller.ts rename to src/schedule/schedule.controller.ts index 0409269..e298093 100644 --- a/src/schedule/v2-schedule.controller.ts +++ b/src/schedule/schedule.controller.ts @@ -1,8 +1,10 @@ import { + Body, Controller, Get, HttpCode, HttpStatus, + NotAcceptableException, Param, Patch, UseGuards, @@ -20,22 +22,22 @@ import { AuthRoles, AuthUnauthorized } from "../auth/auth-role.decorator"; import { UserToken } from "../auth/auth.decorator"; import { UserPipe } from "../auth/auth.pipe"; import { ScheduleService } from "./schedule.service"; -import { ScheduleDto } from "./dto/schedule.dto"; import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager"; -import { UserRole } from "../users/user-role.enum"; -import { User } from "../users/entity/user.entity"; -import { CacheStatusDto } from "./dto/cache-status.dto"; -import { GroupScheduleDto } from "./dto/group-schedule.dto"; -import { ScheduleGroupNamesDto } from "./dto/schedule-group-names.dto"; -import { TeacherScheduleDto } from "./dto/teacher-schedule.dto"; -import { ScheduleTeacherNamesDto } from "./dto/schedule-teacher-names.dto"; -import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2"; +import Schedule from "./entities/schedule.entity"; +import UserRole from "src/users/user-role.enum"; +import GroupSchedule from "./entities/group-schedule.entity"; +import User from "src/users/entity/user.entity"; +import GroupNamesDto from "./dto/get-group-names.dto"; +import TeacherSchedule from "./entities/teacher-schedule.entity"; +import TeacherNamesDto from "./dto/teacher-names.dto"; +import CacheStatusDto from "./dto/cache-status.dto"; +import UpdateDownloadUrlDto from "./dto/update-download-url.dto"; -@ApiTags("v2/schedule") +@ApiTags("v1/schedule") @ApiBearerAuth() -@Controller({ path: "schedule", version: "2" }) +@Controller({ path: "schedule", version: "1" }) @UseGuards(AuthGuard) -export class V2ScheduleController { +export class ScheduleController { constructor(private readonly scheduleService: ScheduleService) {} @ApiOperation({ @@ -45,58 +47,49 @@ export class V2ScheduleController { @ApiResponse({ status: HttpStatus.OK, description: "Расписание получено успешно", - type: ScheduleDto, + type: Schedule, }) - @ResultDto(ScheduleDto) + @ResultDto(Schedule) @AuthRoles([UserRole.ADMIN]) - @CacheKey("v2-schedule") + @CacheKey("schedule") @UseInterceptors(CacheInterceptor) @HttpCode(HttpStatus.OK) @Get() - async getSchedule(): Promise { - return await this.scheduleService.getSchedule().then((result) => - instanceToInstance2(ScheduleDto, result, { - groups: ["v1"], - }), - ); + async getSchedule(): Promise { + return await this.scheduleService.getSchedule(); } @ApiOperation({ summary: "Получение расписания группы" }) @ApiResponse({ status: HttpStatus.OK, description: "Расписание получено успешно", - type: GroupScheduleDto, + type: GroupSchedule, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: "Требуемая группа не найдена", }) - @ResultDto(GroupScheduleDto) + @ResultDto(null) @HttpCode(HttpStatus.OK) @Get("group") async getGroupSchedule( @UserToken(UserPipe) user: User, - ): Promise { - return await this.scheduleService.getGroup(user.group).then((result) => - instanceToInstance2(GroupScheduleDto, result, { - groups: ["v1"], - }), - ); + ): Promise { + return await this.scheduleService.getGroup(user.group); } @ApiOperation({ summary: "Получение списка названий групп" }) @ApiResponse({ status: HttpStatus.OK, description: "Список получен успешно", - type: ScheduleGroupNamesDto, + type: GroupNamesDto, }) - @ResultDto(ScheduleGroupNamesDto) - @CacheKey("v2-schedule-group-names") + @ResultDto(null) + @CacheKey("schedule-group-names") @UseInterceptors(CacheInterceptor) @AuthUnauthorized() - @HttpCode(HttpStatus.OK) @Get("group-names") - async getGroupNames(): Promise { + async getGroupNames(): Promise { return await this.scheduleService.getGroupNames(); } @@ -104,38 +97,40 @@ export class V2ScheduleController { @ApiResponse({ status: HttpStatus.OK, description: "Расписание получено успешно", - type: TeacherScheduleDto, + type: TeacherSchedule, }) @ApiResponse({ status: HttpStatus.NOT_FOUND, description: "Требуемый преподаватель не найден", }) - @ResultDto(TeacherScheduleDto) - @HttpCode(HttpStatus.OK) + @ResultDto(null) @Get("teacher/:name") async getTeacherSchedule( @Param("name") name: string, - ): Promise { - return await this.scheduleService.getTeacher(name).then((result) => - instanceToInstance2(TeacherScheduleDto, result, { - groups: ["v1"], - }), - ); + @UserToken(UserPipe) user: User, + ): Promise { + if (name === "self") { + if (user.role === UserRole.STUDENT) + throw new NotAcceptableException(); + + return await this.scheduleService.getTeacher(user.username); + } + + return await this.scheduleService.getTeacher(name); } @ApiOperation({ summary: "Получение списка ФИО преподавателей" }) @ApiResponse({ status: HttpStatus.OK, description: "Список получен успешно", - type: ScheduleTeacherNamesDto, + type: TeacherNamesDto, }) - @ResultDto(ScheduleTeacherNamesDto) - @CacheKey("v2-schedule-teacher-names") + @ResultDto(null) + @CacheKey("schedule-teacher-names") @UseInterceptors(CacheInterceptor) @AuthUnauthorized() - @HttpCode(HttpStatus.OK) @Get("teacher-names") - async getTeacherNames(): Promise { + async getTeacherNames(): Promise { return await this.scheduleService.getTeacherNames(); } @@ -152,8 +147,10 @@ export class V2ScheduleController { @ResultDto(CacheStatusDto) @HttpCode(HttpStatus.OK) @Patch("update-download-url") - async updateDownloadUrl(): Promise { - return this.scheduleService.getCacheStatus(); + async updateDownloadUrl( + @Body() reqDto: UpdateDownloadUrlDto, + ): Promise { + return await this.scheduleService.updateDownloadUrl(reqDto.url); } @ApiOperation({ @@ -166,7 +163,6 @@ export class V2ScheduleController { type: CacheStatusDto, }) @ResultDto(CacheStatusDto) - @HttpCode(HttpStatus.OK) @Get("cache-status") getCacheStatus(): CacheStatusDto { return this.scheduleService.getCacheStatus(); diff --git a/src/schedule/schedule.module.ts b/src/schedule/schedule.module.ts index 793924a..3d910b4 100644 --- a/src/schedule/schedule.module.ts +++ b/src/schedule/schedule.module.ts @@ -5,19 +5,12 @@ import { UsersModule } from "src/users/users.module"; import { ScheduleReplacerService } from "./schedule-replacer.service"; import { ScheduleReplacerController } from "./schedule-replacer.controller"; import { ScheduleService } from "./schedule.service"; -import { V2ScheduleController } from "./v2-schedule.controller"; -import { V3ScheduleController } from "./v3-schedule.controller"; -import { V4ScheduleController } from "./v4-schedule.controller"; +import { ScheduleController } from "./schedule.controller"; @Module({ imports: [forwardRef(() => UsersModule), FirebaseAdminModule], providers: [PrismaService, ScheduleService, ScheduleReplacerService], - controllers: [ - V2ScheduleController, - V3ScheduleController, - V4ScheduleController, - ScheduleReplacerController, - ], + controllers: [ScheduleController, ScheduleReplacerController], exports: [ScheduleService], }) export class ScheduleModule {} diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts index 7440f04..f3b3547 100644 --- a/src/schedule/schedule.service.ts +++ b/src/schedule/schedule.service.ts @@ -5,21 +5,21 @@ import { plainToInstance } from "class-transformer"; import { ScheduleReplacerService } from "./schedule-replacer.service"; import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service"; import { scheduleConstants } from "../contants"; -import { ScheduleDto } from "./dto/schedule.dto"; import { - V2ScheduleParser, - V2ScheduleParseResult, -} from "./internal/schedule-parser/v2-schedule-parser"; + ScheduleParser, + ScheduleParseResult, +} from "./internal/schedule-parser/schedule-parser"; import * as objectHash from "object-hash"; -import { CacheStatusDto } from "./dto/cache-status.dto"; -import { GroupScheduleDto } from "./dto/group-schedule.dto"; -import { ScheduleGroupNamesDto } from "./dto/schedule-group-names.dto"; -import { TeacherScheduleDto } from "./dto/teacher-schedule.dto"; -import { ScheduleTeacherNamesDto } from "./dto/schedule-teacher-names.dto"; +import CacheStatusDto from "./dto/cache-status.dto"; +import Schedule from "./entities/schedule.entity"; +import GroupSchedule from "./entities/group-schedule.entity"; +import TeacherSchedule from "./entities/teacher-schedule.entity"; +import GetGroupNamesDto from "./dto/get-group-names.dto"; +import TeacherNamesDto from "./dto/teacher-names.dto"; @Injectable() export class ScheduleService { - readonly scheduleParser: V2ScheduleParser; + readonly scheduleParser: ScheduleParser; private cacheUpdatedAt: Date = new Date(0); private cacheHash: string = "0000000000000000000000000000000000000000"; @@ -31,22 +31,24 @@ export class ScheduleService { private readonly scheduleReplacerService: ScheduleReplacerService, private readonly firebaseAdminService: FirebaseAdminService, ) { - setInterval(async () => { + setInterval(() => { const now = new Date(); if (now.getHours() != 7 || now.getMinutes() != 30) return; - await this.firebaseAdminService.sendByTopic("common", { - android: { - priority: "high", - ttl: 60 * 60 * 1000, - }, - data: { - type: "lessons-start", - }, - }); + this.firebaseAdminService + .sendByTopic("common", { + android: { + priority: "high", + ttl: 60 * 60 * 1000, + }, + data: { + type: "lessons-start", + }, + }) + .then(); }, 60000); - this.scheduleParser = new V2ScheduleParser( + this.scheduleParser = new ScheduleParser( new BasicXlsDownloader(), this.scheduleReplacerService, ); @@ -63,7 +65,7 @@ export class ScheduleService { }); } - async getSourceSchedule(): Promise { + async getSourceSchedule(): Promise { const schedule = await this.scheduleParser.getSchedule(); this.cacheUpdatedAt = new Date(); @@ -91,7 +93,7 @@ export class ScheduleService { return schedule; } - async getSchedule(): Promise { + async getSchedule(): Promise { const sourceSchedule = await this.getSourceSchedule(); return { @@ -101,7 +103,7 @@ export class ScheduleService { }; } - async getGroup(name: string): Promise { + async getGroup(name: string): Promise { const schedule = await this.getSourceSchedule(); const group = schedule.groups.get(name); @@ -114,22 +116,22 @@ export class ScheduleService { return { updatedAt: this.cacheUpdatedAt, group: group, - updated: schedule.updatedGroups[name] ?? [], + updated: (schedule.updatedGroups[name] as Array) ?? [], }; } - async getGroupNames(): Promise { + async getGroupNames(): Promise { const schedule = await this.getSourceSchedule(); const names: Array = []; for (const name of schedule.groups.keys()) names.push(name); - return plainToInstance(ScheduleGroupNamesDto, { + return plainToInstance(GetGroupNamesDto, { names: names, }); } - async getTeacher(name: string): Promise { + async getTeacher(name: string): Promise { const schedule = await this.getSourceSchedule(); const teacher = schedule.teachers.get(name); @@ -142,17 +144,20 @@ export class ScheduleService { return { updatedAt: this.cacheUpdatedAt, teacher: teacher, - updated: schedule.updatedGroups[name] ?? [], + updated: (schedule.updatedGroups[name] as Array) ?? [], }; } - async getTeacherNames(): Promise { + async getTeacherNames(): Promise { const schedule = await this.getSourceSchedule(); const names: Array = []; - for (const name of schedule.teachers.keys()) names.push(name); + for (const name of schedule.teachers.keys()) { + if (name === "Ошибка в расписании") continue; + names.push(name); + } - return plainToInstance(ScheduleTeacherNamesDto, { + return plainToInstance(TeacherNamesDto, { names: names, }); } @@ -166,7 +171,7 @@ export class ScheduleService { } async refreshCache() { - await this.cacheManager.reset(); + await this.cacheManager.clear(); await this.getSourceSchedule(); } diff --git a/src/schedule/v3-schedule.controller.ts b/src/schedule/v3-schedule.controller.ts deleted file mode 100644 index d0979d3..0000000 --- a/src/schedule/v3-schedule.controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - Controller, - Get, - HttpCode, - HttpStatus, - Param, - UseGuards, - UseInterceptors, -} from "@nestjs/common"; -import { AuthGuard } from "../auth/auth.guard"; -import { ResultDto } from "../utility/validation/class-validator.interceptor"; -import { - ApiBearerAuth, - ApiOperation, - ApiResponse, - ApiTags, -} from "@nestjs/swagger"; -import { AuthRoles } from "../auth/auth-role.decorator"; -import { UserToken } from "../auth/auth.decorator"; -import { UserPipe } from "../auth/auth.pipe"; -import { ScheduleService } from "./schedule.service"; -import { ScheduleDto } from "./dto/schedule.dto"; -import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager"; -import { UserRole } from "../users/user-role.enum"; -import { User } from "../users/entity/user.entity"; -import { GroupScheduleDto } from "./dto/group-schedule.dto"; -import { TeacherScheduleDto } from "./dto/teacher-schedule.dto"; -import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2"; - -@ApiTags("v3/schedule") -@ApiBearerAuth() -@Controller({ path: "schedule", version: "3" }) -@UseGuards(AuthGuard) -export class V3ScheduleController { - constructor(private readonly scheduleService: ScheduleService) {} - - @ApiOperation({ - summary: "Получение расписания", - tags: ["admin"], - }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Расписание получено успешно", - type: ScheduleDto, - }) - @ResultDto(ScheduleDto) - @AuthRoles([UserRole.ADMIN]) - @CacheKey("v3-schedule") - @UseInterceptors(CacheInterceptor) - @HttpCode(HttpStatus.OK) - @Get() - async getSchedule(): Promise { - return await this.scheduleService.getSchedule().then((result) => - instanceToInstance2(ScheduleDto, result, { - groups: ["v2"], - }), - ); - } - - @ApiOperation({ summary: "Получение расписания группы" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Расписание получено успешно", - type: GroupScheduleDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: "Требуемая группа не найдена", - }) - @ResultDto(GroupScheduleDto) - @HttpCode(HttpStatus.OK) - @Get("group") - async getGroupSchedule( - @UserToken(UserPipe) user: User, - ): Promise { - return await this.scheduleService.getGroup(user.group).then((result) => - instanceToInstance2(GroupScheduleDto, result, { - groups: ["v2"], - }), - ); - } - - @ApiOperation({ summary: "Получение расписания преподавателя" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Расписание получено успешно", - type: TeacherScheduleDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: "Требуемый преподаватель не найден", - }) - @ResultDto(TeacherScheduleDto) - @HttpCode(HttpStatus.OK) - @Get("teacher/:name") - async getTeacherSchedule( - @Param("name") name: string, - ): Promise { - return await this.scheduleService.getTeacher(name).then((result) => - instanceToInstance2(TeacherScheduleDto, result, { - groups: ["v2"], - }), - ); - } -} diff --git a/src/schedule/v4-schedule.controller.ts b/src/schedule/v4-schedule.controller.ts deleted file mode 100644 index 7e773c2..0000000 --- a/src/schedule/v4-schedule.controller.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - Body, - Controller, - Get, - HttpCode, - HttpStatus, - Param, - Patch, - UseGuards, - UseInterceptors, -} from "@nestjs/common"; -import { AuthGuard } from "../auth/auth.guard"; -import { ResultDto } from "../utility/validation/class-validator.interceptor"; -import { - ApiBearerAuth, - ApiOperation, - ApiResponse, - ApiTags, -} from "@nestjs/swagger"; -import { AuthRoles } from "../auth/auth-role.decorator"; -import { UserToken } from "../auth/auth.decorator"; -import { UserPipe } from "../auth/auth.pipe"; -import { ScheduleService } from "./schedule.service"; -import { ScheduleDto } from "./dto/schedule.dto"; -import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager"; -import { UserRole } from "../users/user-role.enum"; -import { User } from "../users/entity/user.entity"; -import { GroupScheduleDto } from "./dto/group-schedule.dto"; -import { TeacherScheduleDto } from "./dto/teacher-schedule.dto"; -import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2"; -import { CacheStatusDto } from "./dto/cache-status.dto"; -import { UpdateDownloadUrlDto } from "./dto/update-download-url.dto"; - -@ApiTags("v4/schedule") -@ApiBearerAuth() -@Controller({ path: "schedule", version: "4" }) -@UseGuards(AuthGuard) -export class V4ScheduleController { - constructor(private readonly scheduleService: ScheduleService) {} - - @ApiOperation({ - summary: "Получение расписания", - tags: ["admin"], - }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Расписание получено успешно", - type: ScheduleDto, - }) - @ResultDto(ScheduleDto) - @AuthRoles([UserRole.ADMIN]) - @CacheKey("v4-schedule") - @UseInterceptors(CacheInterceptor) - @HttpCode(HttpStatus.OK) - @Get() - async getSchedule(): Promise { - return await this.scheduleService.getSchedule().then((result) => - instanceToInstance2(ScheduleDto, result, { - groups: ["v3"], - }), - ); - } - - @ApiOperation({ summary: "Получение расписания группы" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Расписание получено успешно", - type: GroupScheduleDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: "Требуемая группа не найдена", - }) - @ResultDto(GroupScheduleDto) - @HttpCode(HttpStatus.OK) - @Get("group") - async getGroupSchedule( - @UserToken(UserPipe) user: User, - ): Promise { - return await this.scheduleService.getGroup(user.group).then((result) => - instanceToInstance2(GroupScheduleDto, result, { - groups: ["v3"], - }), - ); - } - - @ApiOperation({ summary: "Получение расписания преподавателя" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Расписание получено успешно", - type: TeacherScheduleDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: "Требуемый преподаватель не найден", - }) - @ResultDto(TeacherScheduleDto) - @HttpCode(HttpStatus.OK) - @Get("teacher/:name") - async getTeacherSchedule( - @Param("name") name: string, - ): Promise { - return await this.scheduleService.getTeacher(name).then((result) => - instanceToInstance2(TeacherScheduleDto, result, { - groups: ["v3"], - }), - ); - } - - @ApiOperation({ summary: "Обновление основной страницы политехникума" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Данные обновлены успешно", - type: CacheStatusDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_ACCEPTABLE, - description: "Передан некорректный код страницы", - }) - @ResultDto(CacheStatusDto) - @HttpCode(HttpStatus.OK) - @Patch("update-download-url") - async updateDownloadUrl( - @Body() reqDto: UpdateDownloadUrlDto, - ): Promise { - return await this.scheduleService.updateDownloadUrl(reqDto.url); - } -} diff --git a/src/users/dto/change-group.dto.ts b/src/users/dto/change-group.dto.ts index 667470b..824e5b2 100644 --- a/src/users/dto/change-group.dto.ts +++ b/src/users/dto/change-group.dto.ts @@ -1,4 +1,10 @@ -import { PickType } from "@nestjs/swagger"; -import { User } from "../entity/user.entity"; +import { IsString } from "class-validator"; -export class ChangeGroupDto extends PickType(User, ["group"]) {} +export default class ChangeGroupDto { + /** + * Группа + * @example "ИС-214/23" + */ + @IsString() + group: string; +} diff --git a/src/users/dto/change-username.dto.ts b/src/users/dto/change-username.dto.ts index f14940d..d5bebf2 100644 --- a/src/users/dto/change-username.dto.ts +++ b/src/users/dto/change-username.dto.ts @@ -1,4 +1,12 @@ -import { PickType } from "@nestjs/swagger"; -import { User } from "../entity/user.entity"; +import { IsString, MaxLength, MinLength } from "class-validator"; -export class ChangeUsernameDto extends PickType(User, ["username"]) {} +export default class ChangeUsernameDto { + /** + * Имя + * @example "n08i40k" + */ + @IsString() + @MinLength(1) + @MaxLength(20) + username: string; +} diff --git a/src/users/dto/user.dto.ts b/src/users/dto/user.dto.ts new file mode 100644 index 0000000..f39656d --- /dev/null +++ b/src/users/dto/user.dto.ts @@ -0,0 +1,70 @@ +import { + IsEnum, + IsJWT, + IsMongoId, + IsNumber, + IsOptional, + IsString, + MaxLength, + MinLength, +} from "class-validator"; +import { Exclude, Expose, plainToInstance } from "class-transformer"; +import UserRole from "../user-role.enum"; + +@Exclude() +export default class UserDto { + /** + * Идентификатор (ObjectId) + * @example "66e1b7e255c5d5f1268cce90" + */ + @Expose() + @IsMongoId() + id: string; + + /** + * Имя + * @example "n08i40k" + */ + @Expose() + @IsString() + @MinLength(1) + @MaxLength(20) + username: string; + + /** + * Группа + * @example "ИС-214/23" + */ + @Expose() + @IsString() + group: string; + + /** + * Роль + * @example STUDENT + */ + @Expose() + @IsEnum(UserRole) + role: UserRole; + + /** + * Идентификатор аккаунта VK + * @example "2.0.0" + */ + @Expose() + @IsNumber() + @IsOptional() + vkId?: number; + + /** + * Последний токен доступа + * @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXCJ9..." + */ + @Expose({ groups: ["auth"] }) + @IsJWT({ groups: ["auth"] }) + accessToken: string; + + static fromPlain(plain: object, groups: Array = []): UserDto { + return plainToInstance(UserDto, plain, { groups: groups }); + } +} diff --git a/src/users/dto/v1/v1-client-user.dto.ts b/src/users/dto/v1/v1-client-user.dto.ts deleted file mode 100644 index 34ef030..0000000 --- a/src/users/dto/v1/v1-client-user.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OmitType } from "@nestjs/swagger"; -import { User } from "../../entity/user.entity"; -import { plainToInstance } from "class-transformer"; - -export class V1ClientUserDto extends OmitType(User, [ - "accessToken", - "password", - "salt", - "fcm", - "version", -]) { - static fromUser(userDto: User): V1ClientUserDto { - return plainToInstance(V1ClientUserDto, { - id: userDto.id, - username: userDto.username, - group: userDto.group, - role: userDto.role, - } as V1ClientUserDto); - } -} diff --git a/src/users/dto/v2/v2-client-user.dto.ts b/src/users/dto/v2/v2-client-user.dto.ts deleted file mode 100644 index 9b0026a..0000000 --- a/src/users/dto/v2/v2-client-user.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OmitType } from "@nestjs/swagger"; -import { User } from "../../entity/user.entity"; -import { plainToInstance } from "class-transformer"; - -export class V2ClientUserDto extends OmitType(User, [ - "password", - "salt", - "fcm", - "version", -]) { - static fromUser(userDto: User): V2ClientUserDto { - return plainToInstance(V2ClientUserDto, { - id: userDto.id, - username: userDto.username, - accessToken: userDto.accessToken, - group: userDto.group, - role: userDto.role, - } as V2ClientUserDto); - } -} diff --git a/src/users/entity/fcm-user.entity.ts b/src/users/entity/fcm-user.entity.ts index ec23055..c8283a5 100644 --- a/src/users/entity/fcm-user.entity.ts +++ b/src/users/entity/fcm-user.entity.ts @@ -1,6 +1,7 @@ import { IsArray, IsString, ValidateNested } from "class-validator"; -export class FcmUser { +// noinspection JSClassNamingConvention +export default class FCM { /** * Токен Firebase Cloud Messaging * @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXCJ9..." diff --git a/src/users/entity/user.entity.ts b/src/users/entity/user.entity.ts index 4495d6a..359de46 100644 --- a/src/users/entity/user.entity.ts +++ b/src/users/entity/user.entity.ts @@ -2,6 +2,7 @@ import { IsEnum, IsJWT, IsMongoId, + IsNumber, IsObject, IsOptional, IsSemVer, @@ -9,12 +10,13 @@ import { MaxLength, MinLength, } from "class-validator"; -import { Type } from "class-transformer"; -import { UserRole } from "../user-role.enum"; +import { plainToInstance, Type } from "class-transformer"; +import UserRole from "../user-role.enum"; -import { FcmUser } from "./fcm-user.entity"; +import FCM from "./fcm-user.entity"; +import UserDto from "../dto/user.dto"; -export class User { +export default class User { /** * Идентификатор (ObjectId) * @example "66e1b7e255c5d5f1268cce90" @@ -70,9 +72,9 @@ export class User { * Данные Firebase Cloud Messaging */ @IsObject() - @Type(() => FcmUser) + @Type(() => FCM) @IsOptional() - fcm?: FcmUser; + fcm?: FCM; /** * Версия установленного приложения @@ -80,4 +82,19 @@ export class User { */ @IsSemVer() version: string; + + /** + * Идентификатор аккаунта VK + * @example "2.0.0" + */ + @IsNumber() + vkId?: number; + + static fromPlain(plain: object): User { + return plainToInstance(User, plain); + } + + toDto(groups: Array = []): UserDto { + return plainToInstance(UserDto, this, { groups: groups }); + } } diff --git a/src/users/user-role.enum.ts b/src/users/user-role.enum.ts index 711030a..9c41013 100644 --- a/src/users/user-role.enum.ts +++ b/src/users/user-role.enum.ts @@ -1,5 +1,7 @@ -export enum UserRole { +enum UserRole { STUDENT = "STUDENT", TEACHER = "TEACHER", ADMIN = "ADMIN", } + +export default UserRole; diff --git a/src/users/v1-users.controller.ts b/src/users/users.controller.ts similarity index 52% rename from src/users/v1-users.controller.ts rename to src/users/users.controller.ts index 1df748c..0ba870f 100644 --- a/src/users/v1-users.controller.ts +++ b/src/users/users.controller.ts @@ -1,16 +1,17 @@ import { Body, + ConflictException, Controller, Get, HttpCode, HttpStatus, + NotFoundException, Post, UseGuards, } from "@nestjs/common"; import { AuthGuard } from "../auth/auth.guard"; import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { UserToken } from "../auth/auth.decorator"; -import { AuthService } from "../auth/auth.service"; import { UsersService } from "./users.service"; import { ApiBearerAuth, @@ -19,34 +20,34 @@ import { ApiResponse, ApiTags, } from "@nestjs/swagger"; -import { ChangeUsernameDto } from "./dto/change-username.dto"; -import { ChangeGroupDto } from "./dto/change-group.dto"; -import { V1ClientUserDto } from "./dto/v1/v1-client-user.dto"; -import { UserRole } from "./user-role.enum"; +import User from "./entity/user.entity"; +import ChangeUsernameDto from "./dto/change-username.dto"; +import UserRole from "./user-role.enum"; +import ChangeGroupDto from "./dto/change-group.dto"; +import { UserPipe } from "../auth/auth.pipe"; +import { ScheduleService } from "../schedule/schedule.service"; +import UserDto from "./dto/user.dto"; @ApiTags("v1/users") @ApiBearerAuth() @Controller({ path: "users", version: "1" }) @UseGuards(AuthGuard) -export class V1UsersController { +export class UsersController { constructor( - private readonly authService: AuthService, private readonly usersService: UsersService, + private readonly scheduleService: ScheduleService, ) {} @ApiOperation({ summary: "Получение данных о профиле пользователя" }) @ApiResponse({ status: HttpStatus.OK, description: "Получение профиля прошло успешно", - type: V1ClientUserDto, + type: UserDto, }) - @ResultDto(V1ClientUserDto) - @HttpCode(HttpStatus.OK) + @ResultDto(UserDto) @Get("me") - async getMe(@UserToken() token: string): Promise { - return V1ClientUserDto.fromUser( - await this.authService.decodeUserToken(token), - ); + getMe(@UserToken(UserPipe) user: User): UserDto { + return user.toDto(); } @ApiOperation({ summary: "Смена имени пользователя" }) @@ -63,17 +64,30 @@ export class V1UsersController { @HttpCode(HttpStatus.OK) @Post("change-username") async changeUsername( - @Body() reqDto: ChangeUsernameDto, - @UserToken() token: string, + @Body() changeUsernameDto: ChangeUsernameDto, + @UserToken(UserPipe) user: User, ): Promise { - const user = await this.authService.decodeUserToken(token); - - reqDto.username = + changeUsernameDto.username = user.role == UserRole.ADMIN - ? reqDto.username - : reqDto.username.replace(/\s/g, ""); + ? changeUsernameDto.username + : changeUsernameDto.username.replace(/\s/g, ""); - return await this.usersService.changeUsername(user, reqDto); + if (user.username === changeUsernameDto.username) return; + + if ( + await this.usersService.contains({ + username: changeUsernameDto.username, + }) + ) { + throw new ConflictException( + "Пользователь с таким именем уже существует", + ); + } + + await this.usersService.update({ + where: { id: user.id }, + data: { username: changeUsernameDto.username }, + }); } @ApiOperation({ summary: "Смена группы пользователя" }) @@ -90,11 +104,21 @@ export class V1UsersController { @HttpCode(HttpStatus.OK) @Post("change-group") async changeGroup( - @Body() reqDto: ChangeGroupDto, - @UserToken() token: string, + @Body() changeGroupDto: ChangeGroupDto, + @UserToken(UserPipe) user: User, ): Promise { - const user = await this.authService.decodeUserToken(token); + if (user.group === changeGroupDto.group) return; - return await this.usersService.changeGroup(user, reqDto); + const groupNames = await this.scheduleService.getGroupNames(); + if (!groupNames.names.includes(changeGroupDto.group)) { + throw new NotFoundException( + "Группа с таким названием не существует", + ); + } + + await this.usersService.update({ + where: { id: user.id }, + data: { group: changeGroupDto.group }, + }); } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index dca840f..485582e 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,15 +1,14 @@ import { forwardRef, Module } from "@nestjs/common"; import { UsersService } from "./users.service"; import { PrismaService } from "../prisma/prisma.service"; -import { V2UsersController } from "./v2-users.controller"; import { ScheduleModule } from "../schedule/schedule.module"; import { AuthModule } from "../auth/auth.module"; -import { V1UsersController } from "./v1-users.controller"; +import { UsersController } from "./users.controller"; @Module({ imports: [forwardRef(() => ScheduleModule), forwardRef(() => AuthModule)], providers: [PrismaService, UsersService], exports: [UsersService], - controllers: [V1UsersController, V2UsersController], + controllers: [UsersController], }) export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 213fda7..617527d 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,90 +1,38 @@ -import { - ConflictException, - forwardRef, - Inject, - Injectable, - NotFoundException, -} from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { Prisma } from "@prisma/client"; -import { User } from "./entity/user.entity"; -import { ChangeUsernameDto } from "./dto/change-username.dto"; -import { ChangeGroupDto } from "./dto/change-group.dto"; -import { plainToInstance } from "class-transformer"; -import { ScheduleService } from "../schedule/schedule.service"; +import User from "./entity/user.entity"; @Injectable() export class UsersService { - constructor( - private readonly prismaService: PrismaService, - @Inject(forwardRef(() => ScheduleService)) - private readonly scheduleService: ScheduleService, - ) {} + constructor(private readonly prismaService: PrismaService) {} - async findUnique(where: Prisma.UserWhereUniqueInput): Promise { - return plainToInstance( - User, + async findUnique(where: Prisma.UserWhereUniqueInput): Promise { + return User.fromPlain( await this.prismaService.user.findUnique({ where: where }), ); } + async findOne(where: Prisma.UserWhereInput): Promise { + return User.fromPlain( + await this.prismaService.user.findFirst({ where: where }), + ); + } + async update(params: { where: Prisma.UserWhereUniqueInput; data: Prisma.UserUpdateInput; }): Promise { - return plainToInstance( - User, - await this.prismaService.user.update(params), - ); + return User.fromPlain(await this.prismaService.user.update(params)); } async create(data: Prisma.UserCreateInput): Promise { - return plainToInstance( - User, - await this.prismaService.user.create({ data }), - ); + return User.fromPlain(await this.prismaService.user.create({ data })); } - async contains(where: Prisma.UserWhereUniqueInput): Promise { + async contains(where: Prisma.UserWhereInput): Promise { return await this.prismaService.user .count({ where }) .then((count) => count > 0); } - - async changeUsername( - user: User, - changeUsernameDto: ChangeUsernameDto, - ): Promise { - if (user.username === changeUsernameDto.username) return; - - if (await this.contains({ username: changeUsernameDto.username })) { - throw new ConflictException( - "Пользователь с таким именем уже существует", - ); - } - - await this.update({ - where: { id: user.id }, - data: { username: changeUsernameDto.username }, - }); - } - - async changeGroup( - user: User, - changeGroupDto: ChangeGroupDto, - ): Promise { - if (user.group === changeGroupDto.group) return; - - const groupNames = await this.scheduleService.getGroupNames(); - if (!groupNames.names.includes(changeGroupDto.group)) { - throw new NotFoundException( - "Группа с таким названием не существует", - ); - } - - await this.update({ - where: { id: user.id }, - data: { group: changeGroupDto.group }, - }); - } } diff --git a/src/users/v2-users.controller.ts b/src/users/v2-users.controller.ts deleted file mode 100644 index 33db859..0000000 --- a/src/users/v2-users.controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - Controller, - Get, - HttpCode, - HttpStatus, - UseGuards, -} from "@nestjs/common"; -import { AuthGuard } from "../auth/auth.guard"; -import { ResultDto } from "../utility/validation/class-validator.interceptor"; -import { UserToken } from "../auth/auth.decorator"; -import { AuthService } from "../auth/auth.service"; -import { - ApiBearerAuth, - ApiOperation, - ApiResponse, - ApiTags, -} from "@nestjs/swagger"; -import { V2ClientUserDto } from "./dto/v2/v2-client-user.dto"; - -@ApiTags("v2/users") -@ApiBearerAuth() -@Controller({ path: "users", version: "2" }) -@UseGuards(AuthGuard) -export class V2UsersController { - constructor(private readonly authService: AuthService) {} - - @ApiOperation({ summary: "Получение данных о профиле пользователя" }) - @ApiResponse({ - status: HttpStatus.OK, - description: "Получение профиля прошло успешно", - type: V2ClientUserDto, - }) - @ResultDto(V2ClientUserDto) - @HttpCode(HttpStatus.OK) - @Get("me") - async getMe(@UserToken() token: string): Promise { - return V2ClientUserDto.fromUser( - await this.authService.decodeUserToken(token), - ); - } -} diff --git a/src/utility/class-trasformer/class-transformer-ctor.ts b/src/utility/class-trasformer/class-transformer-ctor.ts new file mode 100644 index 0000000..41bf69d --- /dev/null +++ b/src/utility/class-trasformer/class-transformer-ctor.ts @@ -0,0 +1,33 @@ +import "reflect-metadata"; +import { plainToInstance } from "class-transformer"; + +export type ClassProperties = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + [K in keyof T]: T[K] extends Function ? never : T[K]; +}; + +export class Ctor { + constructor(object: ClassProperties) { + if (object != undefined) + throw new Error( + "You should use ClassTransformerCtor decorator on class to use this feature!", + ); + } +} + +// noinspection FunctionNamingConventionJS +export function ClassTransformerCtor() { + return function (target: any) { + const newCtor: any = function (object: object) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment + const obj: any = new target(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.assign(obj, plainToInstance(target, object)); + + return obj; + }; + + return newCtor; + }; +} diff --git a/src/utility/class-validators/conditional-field.ts b/src/utility/class-validators/conditional-field.ts index 5a36e70..3db25fb 100644 --- a/src/utility/class-validators/conditional-field.ts +++ b/src/utility/class-validators/conditional-field.ts @@ -18,17 +18,12 @@ export function NullIf( constraints: [canBeNull], validator: { validate(value: any, args: ValidationArguments) { - const canBeNullFunc: (cls: object) => boolean = - args.constraints[0]; - - const canBeNull = canBeNullFunc(args.object); - const currentValue = value; + const canBeNull = ( + args.constraints[0] as (cls: object) => boolean + )(args.object); // Логика валидации: если одно из полей null, то другое тоже должно быть null - - return canBeNull - ? currentValue !== null - : currentValue === null; + return canBeNull ? value !== null : value === null; }, defaultMessage(args: ValidationArguments) { return `${args.property} must be ${args.property === null ? "non-null" : "null"}!`; diff --git a/src/utility/validation/class-validator.interceptor.ts b/src/utility/validation/class-validator.interceptor.ts index 849747e..ed27c30 100644 --- a/src/utility/validation/class-validator.interceptor.ts +++ b/src/utility/validation/class-validator.interceptor.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ import "reflect-metadata"; import { @@ -10,7 +11,7 @@ import { UnprocessableEntityException, } from "@nestjs/common"; import { map, Observable } from "rxjs"; -import { instanceToPlain, plainToInstance } from "class-transformer"; +import { instanceToPlain } from "class-transformer"; import { validate, ValidationOptions } from "class-validator"; @Injectable() @@ -32,13 +33,19 @@ export class ClassValidatorInterceptor implements NestInterceptor { handler.name, ); + const providedGroups: Array = Reflect.getMetadata( + "design:result-dto-groups", + cls.prototype, + handler.name, + ); + const isArrayOfDto = Reflect.getMetadata( "design:result-dto-array", cls.prototype, handler.name, ); - if (classDto === null) return returnValue; + if (classDto === null) return instanceToPlain(returnValue); if (classDto === undefined) { console.warn( @@ -47,26 +54,26 @@ export class ClassValidatorInterceptor implements NestInterceptor { return returnValue; } + const groups = [ + ...providedGroups, + ...(this.validatorOptions.groups ?? []), + ]; + const dtoArray: Array = isArrayOfDto ? classDto : [classDto]; for (let idx = 0; idx < dtoArray.length; idx++) { - const returnValueDto = plainToInstance( - dtoArray[idx], - instanceToPlain(returnValue), - ); - - if (!(returnValueDto instanceof Object)) + if (!(returnValue instanceof Object)) throw new InternalServerErrorException( - returnValueDto, + returnValue, "Return value is not object!", ); - const validationErrors = await validate( - returnValueDto, - this.validatorOptions, - ); + const validationErrors = await validate(returnValue, { + ...this.validatorOptions, + groups: groups, + }); if (validationErrors.length > 0) { if (idx !== dtoArray.length - 1) continue; @@ -82,6 +89,7 @@ export class ClassValidatorInterceptor implements NestInterceptor { statusCode: HttpStatus.UNPROCESSABLE_ENTITY, }); } + return returnValue; } }), @@ -90,7 +98,7 @@ export class ClassValidatorInterceptor implements NestInterceptor { } // noinspection FunctionNamingConventionJS -export function ResultDto(dtoType: any) { +export function ResultDto(dtoType: any, groups: Array = []) { return (target: NonNullable, propertyKey: string | symbol) => { Reflect.defineMetadata( "design:result-dto", @@ -104,5 +112,11 @@ export function ResultDto(dtoType: any) { target, propertyKey, ); + Reflect.defineMetadata( + "design:result-dto-groups", + groups, + target, + propertyKey, + ); }; } diff --git a/src/version/response-version.decorator.ts b/src/version/response-version.decorator.ts deleted file mode 100644 index e213551..0000000 --- a/src/version/response-version.decorator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createParamDecorator, ExecutionContext } from "@nestjs/common"; - -export const ResponseVersion = createParamDecorator( - (_, context: ExecutionContext) => { - const sourceVersion: string | null = context.switchToHttp().getRequest() - .headers.version; - const parsedVersion = Number.parseInt(sourceVersion); - - if (Number.isNaN(parsedVersion) || parsedVersion < 0) return 0; - - return parsedVersion; - }, -); diff --git a/test/mainPage b/test/mainPage deleted file mode 100644 index 1b249d6..0000000 --- a/test/mainPage +++ /dev/null @@ -1,1215 +0,0 @@ -PGh0bWw+PHNjcmlwdCBhc3luYz0iIiBzcmM9Imh0dHBzOi8vbWMueWFuZGV4LnJ1L21ldHJpa2Ev -dGFnLmpzIj48L3NjcmlwdD48aGVhZD4KPG1ldGEgY2hhcnNldD0idXRmLTgiPgo8dGl0bGU+0J7R -hNC40YbQuNCw0LvRjNC90YvQuSDRgdCw0LnRgiAtINCg0LDRgdC/0LjRgdCw0L3QuNC1INC30LDQ -vdGP0YLQuNC5PC90aXRsZT4KPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRl -dmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLCBtYXhpbXVtLXNjYWxlPTEiPgoKCjxzY3JpcHQg -dHlwZT0idGV4dC9qYXZhc2NyaXB0Ij4KIHZhciBicm93c2VyID0gbmF2aWdhdG9yLnVzZXJBZ2Vu -dDsKIHZhciBicm93c2VyUmVnZXggPSAvKEFuZHJvaWR8QmxhY2tCZXJyeXxJRU1vYmlsZXxOb2tp -YXxpUChhZHxob25lfG9kKXxPcGVyYSBNKG9iaXxpbmkpKS87CiB2YXIgaXNNb2JpbGUgPSBmYWxz -ZTsKIGlmKGJyb3dzZXIubWF0Y2goYnJvd3NlclJlZ2V4KSkgewogaXNNb2JpbGUgPSB0cnVlOwog -YWRkRXZlbnRMaXN0ZW5lcigibG9hZCIsIGZ1bmN0aW9uKCkgeyBzZXRUaW1lb3V0KGhpZGVVUkxi -YXIsIDApOyB9LCBmYWxzZSk7CiBmdW5jdGlvbiBoaWRlVVJMYmFyKCl7CiB3aW5kb3cuc2Nyb2xs -VG8oMCwxKTsKIH0KIH0KPC9zY3JpcHQ+CjxsaW5rIHR5cGU9InRleHQvY3NzIiByZWw9InN0eWxl -c2hlZXQiIGhyZWY9Ii9fc3QvbXkuY3NzIj4KPHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQi -Pgp2YXIgbmF2VGl0bGUgPSAn0J3QsNCy0LjQs9Cw0YbQuNGPJzsKPC9zY3JpcHQ+CgoJPGxpbmsg -cmVsPSJzdHlsZXNoZWV0IiBocmVmPSIvLnMvc3JjL2Jhc2UubWluLmNzcyI+Cgk8bGluayByZWw9 -InN0eWxlc2hlZXQiIGhyZWY9Ii8ucy9zcmMvbGF5ZXI3Lm1pbi5jc3MiPgoKCTxzY3JpcHQgc3Jj -PSIvLnMvc3JjL2pxdWVyeS0xLjEyLjQubWluLmpzIj48L3NjcmlwdD4KCQoJPHNjcmlwdCBzcmM9 -Ii8ucy9zcmMvdXduZC5taW4uanMiPjwvc2NyaXB0PgoJPHNjcmlwdCBzcmM9Ii8vczc3LnVjb3ou -bmV0L2NnaS91dXRpbHMuZmNnP2E9dVNEJmFtcDtjYT0yJmFtcDt1Zz05OTkmYW1wO2lzcD0xJmFt -cDtyPTAuMTA2MTM2NTg0OTMyMzc2Ij48L3NjcmlwdD4KCTxsaW5rIHJlbD0ic3R5bGVzaGVldCIg -aHJlZj0iLy5zL3NyYy91bGlnaHRib3gvdWxpZ2h0Ym94Lm1pbi5jc3MiPgoJPGxpbmsgcmVsPSJz -dHlsZXNoZWV0IiBocmVmPSIvLnMvc3JjL3NvY2lhbC5jc3MiPgoJPHNjcmlwdCBzcmM9Ii8ucy9z -cmMvdWxpZ2h0Ym94L3VsaWdodGJveC5taW4uanMiPjwvc2NyaXB0PgoJPHNjcmlwdCBzcmM9Ii8u -cy9zcmMvdmlzdWFsbHlfaW1wYWlyZWQubWluLmpzIj48L3NjcmlwdD4KCTxzY3JpcHQ+Ci8qIC0t -LSBVQ09aLUpTLURBVEEgLS0tICovCndpbmRvdy51Q296ID0geyJzaXRlIjp7ImlkIjoiMHB0LWVu -Z2VscyIsImRvbWFpbiI6InBvbGl0ZWhuaWt1bS1lbmcucnUiLCJob3N0IjoicHQtZW5nZWxzLnVj -b3oucnUifSwibW9kdWxlIjoiaW5kZXgiLCJzaWduIjp7IjU0NTgiOiLQodC70LXQtNGD0Y7RidC4 -0LkiLCIzMTI1Ijoi0JfQsNC60YDRi9GC0YwiLCI3MjUzIjoi0J3QsNGH0LDRgtGMINGB0LvQsNC5 -0LTRiNC+0YMiLCI3MjU0Ijoi0JjQt9C80LXQvdC40YLRjCDRgNCw0LfQvNC10YAiLCI3MjUyIjoi -0J/RgNC10LTRi9C00YPRidC40LkiLCI3MjUxIjoi0JfQsNC/0YDQvtGI0LXQvdC90YvQuSDQutC+ -0L3RgtC10L3RgiDQvdC1INC80L7QttC10YIg0LHRi9GC0Ywg0LfQsNCz0YDRg9C20LXQvS4g0J/Q -vtC20LDQu9GD0LnRgdGC0LAsINC/0L7Qv9GA0L7QsdGD0LnRgtC1INC/0L7Qt9C20LUuIiwiNzI4 -NyI6ItCf0LXRgNC10LnRgtC4INC90LAg0YHRgtGA0LDQvdC40YbRgyDRgSDRhNC+0YLQvtCz0YDQ -sNGE0LjQtdC5LiIsIjUyNTUiOiLQn9C+0LzQvtGJ0L3QuNC6In0sImNvdW50cnkiOiJSVSIsInVM -aWdodGJveFR5cGUiOjEsImxhbmd1YWdlIjoicnUiLCJzc2lkIjoiNjA3MjcxNTMzMTUyMzQyMjQy -NzAxIn07Ci8qIC0tLSBVQ09aLUpTLUNPREUgLS0tICovCgl2YXIgdWhlICAgID0gMTsKCXZhciBs -bmcgICAgPSAncnUnOwoJdmFyIGhhcyAgICA9IDA7Cgl2YXIgaW1ncyAgID0gMTsKCXZhciBiZyAg -ICAgPSAxOwoJdmFyIGh3aWR0aCA9IDA7Cgl2YXIgYmdzICAgID0gWzEsIDIgXTsKCXZhciBmb250 -cyAgPSBbMTgsMjQsMjhdOwoJdmFyIGV5ZVNWRyA9ICc8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29k -aW5nPSJ1dGYtOCI/Pjxzdmcgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiB2aWV3Qm94PSIwIDAgMTc1 -MCAxNzUwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGZpbGw9IiNm -ZmZmZmYiIGQ9Ik0xNjY0IDk2MHEtMTUyLTIzNi0zODEtMzUzIDYxIDEwNCA2MSAyMjUgMCAxODUt -MTMxLjUgMzE2LjV0LTMxNi41IDEzMS41LTMxNi41LTEzMS41LTEzMS41LTMxNi41cTAtMTIxIDYx -LTIyNS0yMjkgMTE3LTM4MSAzNTMgMTMzIDIwNSAzMzMuNSAzMjYuNXQ0MzQuNSAxMjEuNSA0MzQu -NS0xMjEuNSAzMzMuNS0zMjYuNXptLTcyMC0zODRxMC0yMC0xNC0zNHQtMzQtMTRxLTEyNSAwLTIx -NC41IDg5LjV0LTg5LjUgMjE0LjVxMCAyMCAxNCAzNHQzNCAxNCAzNC0xNCAxNC0zNHEwLTg2IDYx -LTE0N3QxNDctNjFxMjAgMCAzNC0xNHQxNC0zNHptODQ4IDM4NHEwIDM0LTIwIDY5LTE0MCAyMzAt -Mzc2LjUgMzY4LjV0LTQ5OS41IDEzOC41LTQ5OS41LTEzOS0zNzYuNS0zNjhxLTIwLTM1LTIwLTY5 -dDIwLTY5cTE0MC0yMjkgMzc2LjUtMzY4dDQ5OS41LTEzOSA0OTkuNSAxMzkgMzc2LjUgMzY4cTIw -IDM1IDIwIDY5eiIvPjwvc3ZnPic7CglqUXVlcnkoZnVuY3Rpb24gKCQpIHsKCQlkb2N1bWVudC5i -b2R5Lmluc2VydEFkamFjZW50SFRNTCgnYWZ0ZXJCZWdpbicsICc8YSBpZD0idWh2YiIgY2xhc3M9 -ImluLWJvZHkgcmlnaHQtdG9wICIgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6IzAwMDAwMDsgY29s -b3I6I2ZmZmZmZjsgIiBocmVmPSJqYXZhc2NyaXB0OjsiIG9uY2xpY2s9InV2Y2woKTsiIGl0ZW1w -cm9wPSJjb3B5Ij4nK2V5ZVNWRysnIDxiPtCS0LXRgNGB0LjRjyDQtNC70Y8g0YHQu9Cw0LHQvtCy -0LjQtNGP0YnQuNGFPC9iPjwvYT4nKTsKCQl1aHB2KGhhcyk7Cgl9KTsKCQogZnVuY3Rpb24gdVNv -Y2lhbExvZ2luKHQpIHsKCQkJdmFyIHBhcmFtcyA9IHsidmtvbnRha3RlIjp7ImhlaWdodCI6NDAw -LCJ3aWR0aCI6NzkwfSwib2siOnsid2lkdGgiOjcxMCwiaGVpZ2h0IjozOTB9LCJ5YW5kZXgiOnsi -d2lkdGgiOjg3MCwiaGVpZ2h0Ijo1MTV9LCJnb29nbGUiOnsiaGVpZ2h0Ijo2MDAsIndpZHRoIjo3 -MDB9LCJmYWNlYm9vayI6eyJoZWlnaHQiOjUyMCwid2lkdGgiOjk1MH19OwoJCQl2YXIgcmVmID0g -ZXNjYXBlKGxvY2F0aW9uLnByb3RvY29sICsgJy8vJyArICgncG9saXRlaG5pa3VtLWVuZy5ydScg -fHwgbG9jYXRpb24uaG9zdG5hbWUpICsgbG9jYXRpb24ucGF0aG5hbWUgKyAoKGxvY2F0aW9uLmhh -c2ggPyAoIGxvY2F0aW9uLnNlYXJjaCA/IGxvY2F0aW9uLnNlYXJjaCArICcmJyA6ICc/JyApICsg -J3JuZD0nICsgRGF0ZS5ub3coKSArIGxvY2F0aW9uLmhhc2ggOiAoIGxvY2F0aW9uLnNlYXJjaCB8 -fCAnJyApKSkpOwoJCQl3aW5kb3cub3BlbignLycrdCsnP3JlZj0nK3JlZiwnY29ud2luJywnd2lk -dGg9JytwYXJhbXNbdF0ud2lkdGgrJyxoZWlnaHQ9JytwYXJhbXNbdF0uaGVpZ2h0Kycsc3RhdHVz -PTEscmVzaXphYmxlPTEsbGVmdD0nK3BhcnNlSW50KChzY3JlZW4uYXZhaWxXaWR0aC8yKS0ocGFy -YW1zW3RdLndpZHRoLzIpKSsnLHRvcD0nK3BhcnNlSW50KChzY3JlZW4uYXZhaWxIZWlnaHQvMikt -KHBhcmFtc1t0XS5oZWlnaHQvMiktMjApKydzY3JlZW5YPScrcGFyc2VJbnQoKHNjcmVlbi5hdmFp -bFdpZHRoLzIpLShwYXJhbXNbdF0ud2lkdGgvMikpKycsc2NyZWVuWT0nK3BhcnNlSW50KChzY3Jl -ZW4uYXZhaWxIZWlnaHQvMiktKHBhcmFtc1t0XS5oZWlnaHQvMiktMjApKTsKCQkJcmV0dXJuIGZh -bHNlOwoJCX0KCQlmdW5jdGlvbiBUZWxlZ3JhbUF1dGgodXNlcil7CgkJCXVzZXJbJ2EnXSA9IDk7 -IHVzZXJbJ20nXSA9ICd0ZWxlZ3JhbSc7CgkJCV91UG9zdEZvcm0oJycsIHt0eXBlOiAnUE9TVCcs -IHVybDogJy9pbmRleC9zdWInLCBkYXRhOiB1c2VyfSk7CgkJfQpmdW5jdGlvbiBsb2dpblBvcHVw -Rm9ybShwYXJhbXMgPSB7fSkgeyBuZXcgX3VXbmQoJ0xGJywgJyAnLCAtMjUwLCAtMTAwLCB7IGNs -b3Nlb25lc2M6MSwgcmVzaXplOjEgfSwgeyB1cmw6Jy9pbmRleC80MCcgKyAocGFyYW1zLnVybFBh -cmFtcyA/ICc/JytwYXJhbXMudXJsUGFyYW1zIDogJycpIH0pIH0KLyogLS0tIFVDT1otSlMtRU5E -IC0tLSAqLwo8L3NjcmlwdD4KCgk8c3R5bGU+LlVoaWRlQmxvY2t7ZGlzcGxheTpub25lOyB9PC9z -dHlsZT4KCTxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij5uZXcgSW1hZ2UoKS5zcmMgPSAi -Ly9jb3VudGVyLnlhZHJvLnJ1L2hpdDtub2Fkc3J1P3IiK2VzY2FwZShkb2N1bWVudC5yZWZlcnJl -cikrKHNjcmVlbiYmIjtzIitzY3JlZW4ud2lkdGgrIioiK3NjcmVlbi5oZWlnaHQrIioiKyhzY3Jl -ZW4uY29sb3JEZXB0aHx8c2NyZWVuLnBpeGVsRGVwdGgpKSsiO3UiK2VzY2FwZShkb2N1bWVudC5V -UkwpKyI7IitEYXRlLm5vdygpOzwvc2NyaXB0PjxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0 -Ij5pZihbJ3BvbGl0ZWhuaWt1bS1lbmcucnUnLCdwb2xpdGVobmlrdW0tZW5nLnJ1J10uaW5kZXhP -Zihkb2N1bWVudC5kb21haW4pPDApZG9jdW1lbnQud3JpdGUoJzxtZXRhIGh0dHAtZXF1aXY9InJl -ZnJlc2giIGNvbnRlbnQ9IjA7IHVybD1odHRwczovL3BvbGl0ZWhuaWt1bS1lbmcucnUnK3dpbmRv -dy5sb2NhdGlvbi5wYXRobmFtZSt3aW5kb3cubG9jYXRpb24uc2VhcmNoK3dpbmRvdy5sb2NhdGlv -bi5oYXNoKyciPicpOzwvc2NyaXB0PgoKPHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjxz -dHlsZSB0eXBlPSJ0ZXh0L2NzcyI+PC9zdHlsZT48L2hlYWQ+Cgo8Ym9keSBjbGFzcz0icGFnZS1i -b2R5Ij48YSBpZD0idWh2YiIgY2xhc3M9ImluLWJvZHkgcmlnaHQtdG9wICIgc3R5bGU9ImJhY2tn -cm91bmQtY29sb3I6IHJnYigwLCAwLCAwKTsgY29sb3I6IHJnYigyNTUsIDI1NSwgMjU1KTsgZGlz -cGxheTogaW5saW5lOyIgaHJlZj0iamF2YXNjcmlwdDo7IiBvbmNsaWNrPSJ1dmNsKCk7IiBpdGVt -cHJvcD0iY29weSI+PCEtLT94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPy0tPjxz -dmcgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiB2aWV3Qm94PSIwIDAgMTc1MCAxNzUwIiB4bWxucz0i -aHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGZpbGw9IiNmZmZmZmYiIGQ9Ik0xNjY0 -IDk2MHEtMTUyLTIzNi0zODEtMzUzIDYxIDEwNCA2MSAyMjUgMCAxODUtMTMxLjUgMzE2LjV0LTMx -Ni41IDEzMS41LTMxNi41LTEzMS41LTEzMS41LTMxNi41cTAtMTIxIDYxLTIyNS0yMjkgMTE3LTM4 -MSAzNTMgMTMzIDIwNSAzMzMuNSAzMjYuNXQ0MzQuNSAxMjEuNSA0MzQuNS0xMjEuNSAzMzMuNS0z -MjYuNXptLTcyMC0zODRxMC0yMC0xNC0zNHQtMzQtMTRxLTEyNSAwLTIxNC41IDg5LjV0LTg5LjUg -MjE0LjVxMCAyMCAxNCAzNHQzNCAxNCAzNC0xNCAxNC0zNHEwLTg2IDYxLTE0N3QxNDctNjFxMjAg -MCAzNC0xNHQxNC0zNHptODQ4IDM4NHEwIDM0LTIwIDY5LTE0MCAyMzAtMzc2LjUgMzY4LjV0LTQ5 -OS41IDEzOC41LTQ5OS41LTEzOS0zNzYuNS0zNjhxLTIwLTM1LTIwLTY5dDIwLTY5cTE0MC0yMjkg -Mzc2LjUtMzY4dDQ5OS41LTEzOSA0OTkuNSAxMzkgMzc2LjUgMzY4cTIwIDM1IDIwIDY5eiI+PC9w -YXRoPjwvc3ZnPiA8Yj7QktC10YDRgdC40Y8g0LTQu9GPINGB0LvQsNCx0L7QstC40LTRj9GJ0LjR -hTwvYj48L2E+CjxkaXYgaWQ9InV0YnI4MjE0IiByZWw9InM3NyI+PC9kaXY+CjwhLS1VMUFIRUFE -RVIxWi0tPjxoZWFkZXI+CiAKIDxsaW5rIGhyZWY9Ii8xMi9mb3RvcmFtYS5jc3MiIHJlbD0ic3R5 -bGVzaGVldCI+PHNjcmlwdCBzcmM9Ii8xMi9mb3RvcmFtYS5qcyI+PC9zY3JpcHQ+CjxkaXYgY2xh -c3M9IndyYXBwZXIiPgogPGRpdiBpZD0iaGVhZGVyIj4KIDxkaXYgY2xhc3M9ImhlYWQtbCI+IAog -PHNwYW4gY2xhc3M9InNpdGUtbCI+CiA8c3BhbiBjbGFzcz0ic2l0ZS1uIj48YSBocmVmPSJodHRw -Oi8vcG9saXRlaG5pa3VtLWVuZy5ydS8iPtCe0YTQuNGG0LjQsNC70YzQvdGL0Lkg0YHQsNC50YI8 -L2E+PC9zcGFuPgogPHNwYW4gY2xhc3M9InNpdGUtZCI+PCEtLSA8bG9nbz4gLS0+0JPQkNCf0J7Q -oyDQodCeICLQrdC90LPQtdC70YzRgdGB0LrQuNC5INC/0L7Qu9C40YLQtdGF0L3QuNC60YPQvCI8 -IS0tIDwvbG9nbz4gLS0+PC9zcGFuPgogPC9zcGFuPgogPC9kaXY+CiA8ZGl2IGNsYXNzPSJoZWFk -LXIiPgogPGRpdiBpZD0ic2NoLWJveCI+CiA8ZGl2IGNsYXNzPSJzZWFyY2gtYm94Ij4KIDxkaXYg -Y2xhc3M9InNlYXJjaEZvcm0iPjxmb3JtIG9uc3VibWl0PSJ0aGlzLnNmU2JtLmRpc2FibGVkPXRy -dWUiIG1ldGhvZD0iZ2V0IiBzdHlsZT0ibWFyZ2luOjAiIGFjdGlvbj0iL3NlYXJjaC8iPjxkaXYg -Y2xhc3M9InNjaFF1ZXJ5Ij48aW5wdXQgdmFsdWU9IiIgdHlwZT0idGV4dCIgbmFtZT0icSIgbWF4 -bGVuZ3RoPSIzMCIgc2l6ZT0iMjAiIGNsYXNzPSJxdWVyeUZpZWxkIj48L2Rpdj48ZGl2IGNsYXNz -PSJzY2hCdG4iPjxpbnB1dCB0eXBlPSJzdWJtaXQiIGNsYXNzPSJzZWFyY2hTYm1GbCIgbmFtZT0i -c2ZTYm0iIHZhbHVlPSJTZWFyY2giPjwvZGl2PjwvZm9ybT48L2Rpdj4KIDwvZGl2PgogPC9kaXY+ -CiA8L2Rpdj4KIDxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CiA8L2Rpdj4KIDxuYXY+CiA8ZGl2IGlk -PSJjYXRtZW51Ij48ZGl2IGNsYXNzPSJuYXYtaGVhZCI+PGRpdiBjbGFzcz0iaWNvbiI+PHNwYW4+ -PC9zcGFuPjxzcGFuPjwvc3Bhbj48c3Bhbj48L3NwYW4+PHNwYW4+PC9zcGFuPjwvZGl2PjxhIGhy -ZWY9IiMiPtCd0LDQstC40LPQsNGG0LjRjzwvYT48L2Rpdj4KIDwhLS0gPHNibG9ja19ubWVudT4g -LS0+CjwhLS0gPGJjPiAtLT48ZGl2IGlkPSJ1Tk1lbnVEaXYxIiBjbGFzcz0idU1lbnVWIj48dWwg -Y2xhc3M9InVNZW51Um9vdCI+CjxsaT48YSBocmVmPSIvIiBjbGFzcz0iY3VycmVudC1pdGVtIj48 -c3Bhbj7Qk9C70LDQstC90LDRjyDRgdGC0YDQsNC90LjRhtCwPC9zcGFuPjwvYT48L2xpPgo8bGkg -Y2xhc3M9InVXaXRoU3VibWVudSBpdGVtLXBhcmVudCI+PGEgaHJlZj0iL3N2ZWRlbiIgY2xhc3M9 -IiI+PHNwYW4+0KHQstC10LTQtdC90LjRjyDQvtCxINC+0LHRgNCw0LfQvtCy0LDRgtC10LvRjNC9 -0L7QuSDQvtGA0LPQsNC90LjQt9Cw0YbQuNC4PC9zcGFuPjwvYT48ZW0+KzwvZW0+PHVsIHN0eWxl -PSJkaXNwbGF5OiBub25lOyI+CjxsaSBjbGFzcz0idVdpdGhTdWJtZW51IGl0ZW0tcGFyZW50Ij48 -YSBocmVmPSIvc3ZlZGVuL2NvbW1vbiI+PHNwYW4+0J7RgdC90L7QstC90YvQtSDRgdCy0LXQtNC1 -0L3QuNGPPC9zcGFuPjwvYT48ZW0+KzwvZW0+PHVsPgo8bGk+PGEgaHJlZj0iL2luZGV4L25hdmln -YWNpamEvMC0zMCI+PHNwYW4+0J3QsNCy0LjQs9Cw0YbQuNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+ -PGEgaHJlZj0iL2luZGV4L2tvbnRha3R5LzAtMzMiPjxzcGFuPtCa0L7QvdGC0LDQutGC0Ys8L3Nw -YW4+PC9hPjwvbGk+PC91bD48L2xpPgo8bGk+PGEgaHJlZj0iL3N2ZWRlbi9zdHJ1Y3QiPjxzcGFu -PtCh0YLRgNGD0LrRgtGD0YDQsCDQuCDQvtGA0LPQsNC90Ysg0YPQv9GA0LDQstC70LXQvdC40Y8g -0L7QsdGA0LDQt9C+0LLQsNGC0LXQu9GM0L3QvtC5INC+0YDQs9Cw0L3QuNC30LDRhtC40LXQuTwv -c3Bhbj48L2E+PC9saT4KPGxpIGNsYXNzPSJ1V2l0aFN1Ym1lbnUgaXRlbS1wYXJlbnQiPjxhIGhy -ZWY9Ii9zdmVkZW4vZG9jdW1lbnQiPjxzcGFuPtCU0L7QutGD0LzQtdC90YLRizwvc3Bhbj48L2E+ -PGVtPis8L2VtPjx1bD4KPGxpPjxhIGhyZWY9Ii9pbmRleC91c3Rhdi8wLTM3Ij48c3Bhbj7Qo9GB -0YLQsNCyPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iaHR0cDovL3B0LWVuZ2Vscy51Y296 -LnJ1L2luZGV4L2xpc3RfemFwaXNpX2tfaXptZW5lbmlqYW1fdl91c3Rhdi8wLTMyOCI+PHNwYW4+ -0JvQuNGB0YIg0LfQsNC/0LjRgdC4INC6INC40LfQvNC10L3QtdC90LjRj9C8INCyINGD0YHRgtCw -0LI8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvc3ZpZGV0ZWxzdHZvX29iX2Fr -a3JlZGl0YWNpaS8wLTM2Ij48c3Bhbj7QodCy0LjQtNC10YLQtdC70YzRgdGC0LLQviDQvtCxINCw -0LrQutGA0LXQtNC40YLQsNGG0LjQuDwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRl -eC9saWNlbnppamEvMC0zNSI+PHNwYW4+0JvQuNGG0LXQvdC30LjRjyDQvdCwINC+0YHRg9GJ0LXR -gdGC0LLQu9C10L3QuNC1INC+0LHRgNCw0LfQvtCy0LDRgtC10LvRjNC90L7QuSDQtNC10Y/RgtC1 -0LvRjNC90L7RgdGC0Lg8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvcGxhbl9m -aW5hbnNvdm9fa2hvemphanN0dmVubm9qX2RlamF0ZWxub3N0aS8wLTI0MSI+PHNwYW4+0J/Qu9Cw -0L0g0KTQpdCUPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L29ncm4vMC0xNjEi -PjxzcGFuPtCe0JPQoNCdPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L2lubi8w -LTE1NyI+PHNwYW4+0JjQndCdPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3By -aWthel9vX25hem5hY2hlbmllX3J1a292b2RpdGVsamEvMC0xNjMiPjxzcGFuPtCf0YDQuNC60LDQ -tyDQviDQvdCw0LfQvdCw0YfQtdC90LjQuCDRgNGD0LrQvtCy0L7QtNC40YLQtdC70LXQuTwvc3Bh -bj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9saXN0X3phcGlzaS8wLTE1OCI+PHNwYW4+ -0JvQuNGB0YIg0LfQsNC/0LjRgdC4PC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL3N2ZWRl -bi9sb2NhbCI+PHNwYW4+0JvQvtC60LDQu9GM0L3Ri9C1INC90L7RgNC80LDRgtC40LLQvdGL0LUg -0LDQutGC0Ys8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvc2Ftb29ic2xlZG92 -YW5pZS8wLTY1Ij48c3Bhbj7QntGC0YfQtdGCINC+INGA0LXQt9GD0LvRjNGC0LDRgtCw0YUg0YHQ -sNC80L7QvtCx0YHQu9C10LTQvtCy0LDQvdC40Y88L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVm -PSIvaW5kZXgvMC0yNTciPjxzcGFuPtCf0YDQtdC50YHQutGD0YDQsNC90YI8L3NwYW4+PC9hPjwv -bGk+CjxsaT48YSBocmVmPSIvaW5kZXgva29udHJvbG55ZV9tZXJvcHJpamF0aWphX29yZ2Fub3Zf -Z29zdWRhcnN0dmVubm9nb19rb250cm9samFfbmFkem9yYV92X3NmZXJlX29icmF6b3ZhbmlqYS8w -LTEzOSI+PHNwYW4+0JrQvtC90YLRgNC+0LvRjNC90YvQtSDQvNC10YDQvtC/0YDQuNGP0YLQuNGP -INC+0YDQs9Cw0L3QvtCyINCz0L7RgdGD0LTQsNGA0YHRgtCy0LXQvdC90L7Qs9C+INC60L7QvdGC -0YDQvtC70Y8o0L3QsNC00LfQvtGA0LApINCyINGB0YTQtdGA0LUg0L7QsdGA0LDQt9C+0LLQsNC9 -0LjRjzwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9zYW5pdGFybm9fZWhwaWRl -bWlvbG9naWNoZXNrb2VfemFrbGp1Y2hlbmllLzAtMjYyIj48c3Bhbj7QodCw0L3QuNGC0LDRgNC9 -0L4t0Y3Qv9C40LTQtdC80LjQvtC70L7Qs9C40YfQtdGB0LrQvtC1INC30LDQutC70Y7Rh9C10L3Q -uNC1PC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3RyZWJvdmFuaWphX3Bvemhh -cm5val9iZXpvcGFzbm9zdGkvMC00ODUiPjxzcGFuPtCi0YDQtdCx0L7QstCw0L3QuNGPINC/0L7Q -ttCw0YDQvdC+0Lkg0LHQtdC30L7Qv9Cw0YHQvdC+0YHRgtC4PC9zcGFuPjwvYT48L2xpPgo8bGk+ -PGEgaHJlZj0iL2luZGV4L3JlenVsdGF0eV9zcGVjb2NlbmtpX3VzbG92aWpfdHJ1ZGEvMC01OTAi -PjxzcGFuPtCg0LXQt9GD0LvRjNGC0LDRgtGLINGB0L/QtdGG0L7RhtC10L3QutC4INGD0YHQu9C+ -0LLQuNC5INGC0YDRg9C00LA8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvbm9y -bWF0aXZueWVfcHJhdm92eWVfZG9rdW1lbnR5X3BvX29raHJhbmVfdHJ1ZGEvMC02MDMiPjxzcGFu -PtCd0L7RgNC80LDRgtC40LLQvdGL0LUg0L/RgNCw0LLQvtCy0YvQtSDQtNC+0LrRg9C80LXQvdGC -0Ysg0L/QviDQvtGF0YDQsNC90LUg0YLRgNGD0LTQsDwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhy -ZWY9Ii8yMDIzL3NodGF0bm9lX3Jhc3AucGRmIj48c3Bhbj7QqNGC0LDRgtC90L7QtSDRgNCw0YHQ -v9C40YHQsNC90LjQtTwvc3Bhbj48L2E+PC9saT48L3VsPjwvbGk+CjxsaT48YSBocmVmPSIvc3Zl -ZGVuL2VkdWNhdGlvbiI+PHNwYW4+0J7QsdGA0LDQt9C+0LLQsNC90LjQtTwvc3Bhbj48L2E+PC9s -aT4KPGxpPjxhIGhyZWY9Ii9zdmVkZW4vZWR1U3RhbmRhcnRzIj48c3Bhbj7QntCx0YDQsNC30L7Q -stCw0YLQtdC70YzQvdGL0LUg0YHRgtCw0L3QtNCw0YDRgtGLINC4INGC0YDQtdCx0L7QstCw0L3Q -uNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL3N2ZWRlbi9lbXBsb3llZXMiPjxzcGFu -PtCg0YPQutC+0LLQvtC00YHRgtCy0L48L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5k -ZXgvMC00NSI+PHNwYW4+0J/QtdC00LDQs9C+0LPQuNGH0LXRgdC60LjQtSDRgNCw0LHQvtGC0L3Q -uNC60Lg8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvc3ZlZGVuL29iamVjdHMiPjxzcGFu -PtCc0LDRgtC10YDQuNCw0LvRjNC90L4t0YLQtdGF0L3QuNGH0LXRgdC60L7QtSDQvtCx0LXRgdC/ -0LXRh9C10L3QuNC1INC4INC+0YHQvdCw0YnQtdC90L3QvtGB0YLRjCDQvtCx0YDQsNC30L7QstCw -0YLQtdC70YzQvdC+0LPQviDQv9GA0L7RhtC10YHRgdCwPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEg -aHJlZj0iL3N2ZWRlbi9ncmFudHMiPjxzcGFuPtCh0YLQuNC/0LXQvdC00LjQuCDQuCDQvNC10YDR -iyDQv9C+0LTQtNC10YDQttC60Lgg0L7QsdGD0YfQsNGO0YnQuNGF0YHRjzwvc3Bhbj48L2E+PC9s -aT4KPGxpIGNsYXNzPSJ1V2l0aFN1Ym1lbnUgaXRlbS1wYXJlbnQiPjxhIGhyZWY9Ii9zdmVkZW4v -cGFpZF9lZHUiPjxzcGFuPtCf0LvQsNGC0L3Ri9C1INC+0LHRgNCw0LfQvtCy0LDRgtC10LvRjNC9 -0YvQtSDRg9GB0LvRg9Cz0Lg8L3NwYW4+PC9hPjxlbT4rPC9lbT48dWw+CjxsaT48YSBocmVmPSIv -ZG9wX29icmF6L3BvbG96aGVuaWVfb19yZXN1cnNub21fY2VudHJlLnBkZiI+PHNwYW4+0J/QvtC7 -0L7QttC10L3QuNC1INC+INGA0LXRgdGD0YDRgdC90L7QvCDRhtC10L3RgtGA0LU8L3NwYW4+PC9h -PjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvZG9nb3Zvcl9uYV9va2F6YW5pZV9wbGF0bnlraF91 -c2x1Zy8wLTU3MCI+PHNwYW4+0JTQvtCz0L7QstC+0YAg0L3QsCDQvtC60LDQt9Cw0L3QuNC1INC/ -0LvQsNGC0L3Ri9GFINGD0YHQu9GD0LM8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvMTIv -cHJpa2F6X29fcGxhdG55a2hfdXNsdWdha2gucGRmP3Zlcj0wMTA2MjQiPjxzcGFuPtCf0YDQuNC6 -0LDQtyDQvtCxINC+0LrQsNC30LDQvdC40Lgg0L/Qu9Cw0YLQvdGL0YUg0L7QsdGA0LDQt9C+0LLQ -sNGC0LXQu9GM0L3Ri9GFINGD0YHQu9GD0LM8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIv -MjAyNC9wcmlrYXpfNTE4X29iX3VzdGFub3ZsZW5paV9wbGF0eS5wZGYiPjxzcGFuPtCf0YDQuNC6 -0LDQtyDQntCxINGD0YHRgtCw0L3QvtCy0LvQtdC90LjQuCDQv9C70LDRgtGLINC30LAg0L/RgNC+ -0LbQuNCy0LDQvdC40LUg0LIg0L7QsdGJ0LXQttC40YLQuNC4PC9zcGFuPjwvYT48L2xpPgo8bGk+ -PGEgaHJlZj0iL2luZGV4L3Jhc3Bpc2FuaWVfemFuamF0aWpfcG9fcHBvLzAtNjAxIj48c3Bhbj7Q -oNCw0YHQv9C40YHQsNC90LjQtSDQt9Cw0L3Rj9GC0LjQuSDQv9C+INCf0J/Qnjwvc3Bhbj48L2E+ -PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9wcmVqc2t1cmFudC8wLTI1NyI+PHNwYW4+0J/RgNC1 -0LnRgdC60YPRgNCw0L3Rgjwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9yYWJv -Y2hpZV9wcm9ncmFtbXkvMC0yNTgiPjxzcGFuPtCf0YDQvtCz0YDQsNC80LzRiyDQv9GA0L7RhNC1 -0YHRgdC40L7QvdCw0LvRjNC90L7Qs9C+INC+0LHRg9GH0LXQvdC40Y88L3NwYW4+PC9hPjwvbGk+ -CjxsaT48YSBocmVmPSIvaW5kZXgvcHJvZ3JhbW15X2RvcG9sbml0ZWxub2dvX29icmF6b3Zhbmlq -YS8wLTYzMiI+PHNwYW4+0J/RgNC+0LPRgNCw0LzQvNGLINC00L7Qv9C+0LvQvdC40YLQtdC70YzQ -vdC+0LPQviDQvtCx0YDQsNC30L7QstCw0L3QuNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJl -Zj0iL2luZGV4L2FuYWxpel9rb2xpY2hlc3R2YS8wLTI2MCI+PHNwYW4+0JDQvdCw0LvQuNC3INC6 -0L7Qu9C40YfQtdGB0YLQstCwPC9zcGFuPjwvYT48L2xpPjwvdWw+PC9saT4KPGxpIGNsYXNzPSJ1 -V2l0aFN1Ym1lbnUgaXRlbS1wYXJlbnQiPjxhIGhyZWY9Ii9zdmVkZW4vYnVkZ2V0Ij48c3Bhbj7Q -pNC40L3QsNC90YHQvtCy0L4t0YXQvtC30Y/QudGB0YLQstC10L3QvdCw0Y8g0LTQtdGP0YLQtdC7 -0YzQvdC+0YHRgtGMPC9zcGFuPjwvYT48ZW0+KzwvZW0+PHVsPgo8bGk+PGEgaHJlZj0iL2luZGV4 -L3NvZ2xhc2hlbmllLzAtMjYzIj48c3Bhbj7QodC+0LPQu9Cw0YjQtdC90LjRjzwvc3Bhbj48L2E+ -PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9wbGFuX2ZraGRfbmFfMjAxNF9nb2QvMC0yNjQiPjxz -cGFuPtCf0LvQsNC9INCk0KXQlDwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9v -dGNoZXRfb2JfaXNwb2xuZW5paV9wZmtoZF9uYV8yMDE0X2dvZC8wLTI2NSI+PHNwYW4+0J7RgtGH -0LXRgiDQvtCxINC40YHQv9C+0LvQvdC10L3QuNC4INCf0KTQpdCUPC9zcGFuPjwvYT48L2xpPgo8 -bGk+PGEgaHJlZj0iL2luZGV4L2luZm9ybWFjaWphX29fcG9zdHVwbGVuaWlfaV9yYXNraG9kb3Zh -bmlpX2ZpbmFuc292eWtoX2lfbWF0ZXJpYWxueWtoX3NyZWRzdHZfcG9faXRvZ2FtX2ZpbmFuc292 -b2dvX2dvZGEvMC02MzciPjxzcGFuPtCY0L3RhNC+0YDQvNCw0YbQuNGPINC+INC/0L7RgdGC0YPQ -v9C70LXQvdC40Lgg0Lgg0YDQsNGB0YXQvtC00L7QstCw0L3QuNC4INGE0LjQvdCw0L3RgdC+0LLR -i9GFINC4INC80LDRgtC10YDQuNCw0LvRjNC90YvRhSDRgdGA0LXQtNGB0YLQsiDQv9C+INC40YLQ -vtCz0LDQvCDRhNC40L3QsNC90YHQvtCy0L7Qs9C+INCz0L7QtNCwPC9zcGFuPjwvYT48L2xpPjwv -dWw+PC9saT4KPGxpPjxhIGhyZWY9Imh0dHA6Ly9wb2xpdGVobmlrdW0tZW5nLnJ1L3N2ZWRlbi92 -YWNhbnQiPjxzcGFuPtCS0LDQutCw0L3RgtC90YvQtSDQvNC10YHRgtCwINC00LvRjyDQv9GA0LjQ -tdC80LAgKNC/0LXRgNC10LLQvtC00LApPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2lu -ZGV4L29yZ2FuaXphY2lqYV9waXRhbmlqYV92X29icmF6b3ZhdGVsbm9qX29yZ2FuaXphY2lpLzAt -NjAyIj48c3Bhbj7QntGA0LPQsNC90LjQt9Cw0YbQuNGPINC/0LjRgtCw0L3QuNGPINCyINC+0LHR -gNCw0LfQvtCy0LDRgtC10LvRjNC90L7QuSDQvtGA0LPQsNC90LjQt9Cw0YbQuNC4PC9zcGFuPjwv -YT48L2xpPgo8bGk+PGEgaHJlZj0iL3N2ZWRlbi9vdnoiPjxzcGFuPtCU0L7RgdGC0YPQv9C90LDR -jyDRgdGA0LXQtNCwPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL3N2ZWRlbi9pbnRlciI+ -PHNwYW4+0JzQtdC20LTRg9C90LDRgNC+0LTQvdC+0LUg0YHQvtGC0YDRg9C00L3QuNGH0LXRgdGC -0LLQvjwvc3Bhbj48L2E+PC9saT48L3VsPjwvbGk+CjxsaSBjbGFzcz0idVdpdGhTdWJtZW51IGl0 -ZW0tcGFyZW50Ij48YSBocmVmPSIvaW5kZXgvbWV0b2RpY2hlc2thamFfcmFib3RhLzAtMjU0Ij48 -c3Bhbj7QnNC10YLQvtC00LjRh9C10YHQutCw0Y8g0YDQsNCx0L7RgtCwPC9zcGFuPjwvYT48ZW0+ -KzwvZW0+PHVsPgo8bGkgY2xhc3M9InVXaXRoU3VibWVudSBpdGVtLXBhcmVudCI+PGEgaHJlZj0i -L2luZGV4LzAtNzAiPjxzcGFuPtCf0KbQmjwvc3Bhbj48L2E+PGVtPis8L2VtPjx1bD4KPGxpPjxh -IGhyZWY9Ii9pbmRleC8wLTczIj48c3Bhbj7Qn9Cm0Jog0LPRg9C80LDQvdC40YLQsNGA0L3Ri9GF -INC00LjRgdGG0LjQv9C70LjQvTwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8w -LTc2Ij48c3Bhbj7Qn9Cm0Jog0LjQvdGE0L7RgNC80LDRhtC40L7QvdC90L4t0LrQvtC80LzRg9C9 -0LjQutCw0YbQuNC+0L3QvdGL0YUg0YLQtdGF0L3QvtC70L7Qs9C40Lk8L3NwYW4+PC9hPjwvbGk+ -CjxsaT48YSBocmVmPSIvaW5kZXgvMC03NCI+PHNwYW4+0J/QptCaINC10YHRgtC10YHRgtCy0LXQ -vdC90L7QvdCw0YPRh9C90YvRhSDQtNC40YHRhtC40L/Qu9C40L08L3NwYW4+PC9hPjwvbGk+Cjxs -aT48YSBocmVmPSIvaW5kZXgvMC03NSI+PHNwYW4+0J/QptCaINGB0YTQtdGA0Ysg0YPRgdC70YPQ -szwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTM4MiI+PHNwYW4+0J/QptCa -INCw0LLRgtC+0LzQvtCx0LjQu9GM0L3Ri9GFINC4INGB0YLRgNC+0LjRgtC10LvRjNC90YvRhSDQ -v9GA0L7RhNC10YHRgdC40Lkv0YHQv9C10YbQuNCw0LvRjNC90L7RgdGC0LXQuTwvc3Bhbj48L2E+ -PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTU3NiI+PHNwYW4+0J/QptCaINGB0YLRgNC+0LjR -gtC10LvRjNC90YvRhSDQv9GA0L7RhNC10YHRgdC40Lkg0Lgg0YHQv9C10YbQuNCw0LvRjNC90L7R -gdGC0LXQuTwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTc4Ij48c3Bhbj7Q -n9Cm0Jog0LrQu9Cw0YHRgdC90YvRhSDRgNGD0LrQvtCy0L7QtNC40YLQtdC70LXQuSDQuCDQutGD -0YDQsNGC0L7RgNC+0LI8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvMC0zODMi -PjxzcGFuPtCf0KbQmiDRgdC+0YbQuNCw0LvRjNC90L4t0Y3QutC+0L3QvtC80LjRh9C10YHQutC4 -0YUg0LTQuNGB0YbQuNC/0LvQuNC9PC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4 -LzAtMzg0Ij48c3Bhbj7Qn9Cm0Jog0YHRhNC10YDRiyDQvtCx0YnQtdGB0YLQstC10L3QvdC+0LPQ -viDQv9C40YLQsNC90LjRjzwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTM4 -NSI+PHNwYW4+0J/QptCaINGE0LjQt9C40YfQtdGB0LrQvtC5INC60YPQu9GM0YLRg9GA0Ysg0Lgg -0JHQltCUPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3Bja19hZWhyb25hdmln -YWNpaV9pX2Voa3NwbHVhdGFjaWlfYXZpYWNpb25ub2pfaV9yYWtldG5vX2tvc21pY2hlc2tval90 -ZWtobmlraS8wLTY1NyI+PHNwYW4+0J/QptCaINCw0Y3RgNC+0L3QsNCy0LjQs9Cw0YbQuNC4INC4 -INGN0LrRgdC/0LvRg9Cw0YLQsNGG0LjQuCDQsNCy0LjQsNGG0LjQvtC90L3QvtC5INC4INGA0LDQ -utC10YLQvdC+LdC60L7RgdC80LjRh9C10YHQutC+0Lkg0YLQtdGF0L3QuNC60Lg8L3NwYW4+PC9h -PjwvbGk+PC91bD48L2xpPgo8bGkgY2xhc3M9InVXaXRoU3VibWVudSBpdGVtLXBhcmVudCI+PGEg -aHJlZj0iaHR0cDovL3BvbGl0ZWhuaWt1bS1lbmcucnUvaW5kZXgvbWV0b2RpY2hlc2tvZV9vYmVz -cGVjaGVuaWUvMC0zNzEiPjxzcGFuPtCc0LXRgtC+0LTQuNGH0LXRgdC60L7QtSDQvtCx0LXRgdC/ -0LXRh9C10L3QuNC1PC9zcGFuPjwvYT48ZW0+KzwvZW0+PHVsPgo8bGk+PGEgaHJlZj0iaHR0cDov -L3BvbGl0ZWhuaWt1bS1lbmcucnUvaW5kZXgvMC0zNzgiPjxzcGFuPtCf0YDQtdC/0L7QtNCw0LLQ -sNGC0LXQu9GOPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iaHR0cDovL3BvbGl0ZWhuaWt1 -bS1lbmcucnUvaW5kZXgvMC0zNzkiPjxzcGFuPtCc0LDRgdGC0LXRgNGDINC/L9C+PC9zcGFuPjwv -YT48L2xpPgo8bGk+PGEgaHJlZj0iaHR0cDovL3BvbGl0ZWhuaWt1bS1lbmcucnUvaW5kZXgvMC0z -ODAiPjxzcGFuPtCh0YLRg9C00LXQvdGC0YM8L3NwYW4+PC9hPjwvbGk+PC91bD48L2xpPgo8bGk+ -PGEgaHJlZj0iaHR0cDovL3BvbGl0ZWhuaWt1bS1lbmcucnUvaW5kZXgvMC00NTEiPjxzcGFuPtCV -0LTQuNC90LDRjyDQvNC10YLQvtC00LjRh9C10YHQutCw0Y8g0YLQtdC80LA8L3NwYW4+PC9hPjwv -bGk+CjxsaT48YSBocmVmPSIvcGVkc292ZXQvcGxhbl9tZXRvZHJhYm90eS5wZGY/dmVyPTciPjxz -cGFuPtCf0LvQsNC9INC80LXRgtC+0LTQuNGH0LXRgdC60L7QuSDRgNCw0LHQvtGC0Ys8L3NwYW4+ -PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvb2JyYXpjeV9wcm92ZXJvY2hueWtoX3JhYm90 -X2RsamFfcHJvdmVkZW5pamFfdnByX3Nwby8wLTYxNSI+PHNwYW4+0J7QsdGA0LDQt9GG0Ysg0L/R -gNC+0LLQtdGA0L7Rh9C90YvRhSDRgNCw0LHQvtGCINC00LvRjyDQv9GA0L7QstC10LTQtdC90LjR -jyDQktCf0KAg0KHQn9CePC9zcGFuPjwvYT48L2xpPjwvdWw+PC9saT4KPGxpIGNsYXNzPSJ1V2l0 -aFN1Ym1lbnUgaXRlbS1wYXJlbnQiPjxhIGhyZWY9Ii9pbmRleC9wcmllbW5hamFfa29taXNzaWph -LzAtNTQiIGNsYXNzPSIiPjxzcGFuPtCf0YDQuNC10LzQvdCw0Y8g0LrQvtC80LjRgdGB0LjRjzwv -c3Bhbj48L2E+PGVtPis8L2VtPjx1bCBzdHlsZT0iZGlzcGxheTogbm9uZTsiPgo8bGk+PGEgaHJl -Zj0iLzIwMjQvcHJpa2F6LTkxLWtfb196YWNoaXNsZW5paV9uYV8xX2t1cnNfb2J1YWp1c2hoaWto -c2oucGRmP3Zlcj0yIj48c3Bhbj7Qn9Cg0JjQmtCQ0Jcg4oSWIDkxLdCaJm5ic3A7ItCeJm5ic3A7 -0LfQsNGH0LjRgdC70LXQvdC40Lgg0L7QsdGD0YfQsNGO0YnQuNGF0YHRjyDQvdCwIDEg0LrRg9GA -0YEgMjAyNCDQsy4iPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iLzIwMjQvcHJpa2F6LTky -LWtfb196YWNoaXNsZW5paV9zbHVzaGF0ZWxlal9uYV8xX2t1cnMucGRmIj48c3Bhbj7Qn9Cg0JjQ -mtCQ0JcmbmJzcDvihJYgOTIt0JogItCeJm5ic3A70LfQsNGH0LjRgdC70LXQvdC40Lgg0YHQu9GD -0YjQsNGC0LXQu9C10Lkg0L3QsCAxINC60YPRgNGBIDIwMjQg0LMuIjwvc3Bhbj48L2E+PC9saT4K -PGxpPjxhIGhyZWY9Ii8yMDI0L3Byb2VrdF9wcmlrYXphLnBkZj92ZXI9MSI+PHNwYW4+0J/QoNCY -0JrQkNCXIOKEljEwMy3QuiZuYnNwOyLQniZuYnNwO9C30LDRh9C40YHQu9C10L3QuNC4INC+0LHR -g9GH0LDRjtGJ0LjRhdGB0Y8g0L3QsCDQv9C10YDQstGL0Lkg0LrRg9GA0YEmbmJzcDsyMDI0INCz -Ljwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9wZXJlY2hlbl9kb2t1bWVudG92 -X2RsamFfcG9zdHVwbGVuaWphLzAtMjEyIj48c3Bhbj7Qn9C10YDQtdGH0LXQvdGMINC00L7QutGD -0LzQtdC90YLQvtCyINC00LvRjyDQv9C+0YHRgtGD0L/Qu9C10L3QuNGPPC9zcGFuPjwvYT48L2xp -Pgo8bGk+PGEgaHJlZj0iLzIwMjMvcHJhdmlsYV9wcmllbWEucGRmP3Zlcj02Ij48c3Bhbj7Qn9GA -0LDQstC40LvQsCDQv9GA0LjQtdC80LA8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvMjAy -NC9pem1lbmVuaWphX3ZfcHJhdmlsYV9wcmllbWFfbmFfMjAyNF9nLnBkZiI+PHNwYW4+0JjQt9C8 -0LXQvdC10L3QuNGPINCyINC/0YDQsNCy0LjQu9CwINC/0YDQuNC10LzQsCDQs9GA0LDQttC00LDQ -vTwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9jZWxldm9lX29idWNoZW5pZS8w -LTY1NCI+PHNwYW4+0KbQtdC70LXQstC+0LUg0L7QsdGD0YfQtdC90LjQtTwvc3Bhbj48L2E+PC9s -aT4KPGxpPjxhIGhyZWY9Ii8yMDIzL3ByYXZpbGFfcHJpZW1hX25hX29idWNoX292el8yMDIzLnBk -Zj92ZXI9NiI+PHNwYW4+0J/RgNCw0LLQuNC70LAg0L/RgNC40LXQvNCwINC70LjRhiDRgSDQntCS -0Jc8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSJodHRwOi8vcG9saXRlaG5pa3VtLWVuZy5y -dS9pbmRleC9wcmllbV96YWphdmxlbmlqX2lfbmVvYmtob2RpbXlraF9kb2t1bWVudG92X3ZfZWhs -ZWt0cm9ubm9qX2Zvcm1lLzAtNjE5Ij48c3Bhbj7QmNC90YTQvtGA0LzQsNGG0LjRjyDQviDQstC+ -0LfQvNC+0LbQvdC+0YHRgtC4INC/0YDQuNC10LzQsCDQt9Cw0Y/QstC70LXQvdC40Lkg0LIg0Y3Q -u9C10LrRgtGA0L7QvdC90L7QuSDRhNC+0YDQvNC1PC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJl -Zj0iL2luZGV4LzAtMTMxIj48c3Bhbj7Qn9C10YDQtdGH0LXQvdGMINGB0L/QtdGG0LjQsNC70YzQ -vdC+0YHRgtC10Lkv0L/RgNC+0YTQtdGB0YHQuNC5PC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJl -Zj0iaHR0cDovL3BvbGl0ZWhuaWt1bS1lbmcucnUvaW5kZXgvb2JzaGhpZV9wcmF2aWxhX3BvZGFj -aGlfaV9yYXNzbW90cmVuaWphX2FwZWxsamFjaWovMC00NDYiPjxzcGFuPtCe0LHRidC40LUg0L/R -gNCw0LLQuNC70LAg0L/QvtC00LDRh9C4INCw0L/QtdC70LvRj9GG0LjQuDwvc3Bhbj48L2E+PC9s -aT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTUzNyI+PHNwYW4+0KHQstC10LTQtdC90LjRjyDQviDQ -utC+0LvQuNGH0LXRgdGC0LLQtSDQv9C+0LTQsNC90L3Ri9GFINC30LDRj9Cy0LvQtdC90LjQuTwv -c3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTU4MCI+PHNwYW4+0KPRgdC70L7Q -stC40Y8g0L/RgNC40LXQvNCwINC90LAg0L7QsdGD0YfQtdC90LjQtSAg0L/QviDQtNC+0LPQvtCy -0L7RgNCw0Lwg0L7QsSDQvtC60LDQt9Cw0L3QuNC4INC/0LvQsNGC0L3Ri9GFINC+0LHRgNCw0LfQ -vtCy0LDRgtC10LvRjNC90YvRhSDRg9GB0LvRg9CzPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJl -Zj0iL2luZGV4L3Jhc3Bpc2FuaWVfdnN0dXBpdGVsbnlraF9pc3B5dGFuaWovMC01ODYiPjxzcGFu -PtCg0LDRgdC/0LjRgdCw0L3QuNC1INCy0YHRgtGD0L/QuNGC0LXQu9GM0L3Ri9GFINC40YHQv9GL -0YLQsNC90LjQuTwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9lZHVjYXRpb24va2NwX25h -XzIwMjQucGRmP3Zlcj01Ij48c3Bhbj7QmtC+0L3RgtGA0L7Qu9GM0L3Ri9C1INGG0LjRhNGA0Ysg -0L/RgNC40LXQvNCwINCz0YDQsNC20LTQsNC9INC90LAgMjAyNCDQs9C+0LQ8L3NwYW4+PC9hPjwv -bGk+PC91bD48L2xpPgo8bGkgY2xhc3M9InVXaXRoU3VibWVudSBpdGVtLXBhcmVudCI+PGEgaHJl -Zj0iL2luZGV4L2F2dG9zaGtvbGEvMC0yODYiIGNsYXNzPSIiPjxzcGFuPtCQ0LLRgtC+0YjQutC+ -0LvQsDwvc3Bhbj48L2E+PGVtPis8L2VtPjx1bCBzdHlsZT0iZGlzcGxheTogbm9uZTsiPgo8bGk+ -PGEgaHJlZj0iaHR0cDovL3B0LWVuZ2Vscy51Y296LnJ1L2luZGV4L2ZvdG9tYXRlcmlhbHkvMC0z -MzciPjxzcGFuPtCk0L7RgtC+0LzQsNGC0LXRgNC40LDQu9GLPC9zcGFuPjwvYT48L2xpPgo8bGk+ -PGEgaHJlZj0iL2luZGV4LzAtNDg2Ij48c3Bhbj7QoNCw0LHQvtGH0LjQtSDQv9GA0L7Qs9GA0LDQ -vNC80Ys8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSJodHRwOi8vcG9saXRlaG5pa3VtLWVu -Zy5ydS9pbmRleC8wLTM4MSI+PHNwYW4+0JfQsNC60LvRjtGH0LXQvdC40LU8L3NwYW4+PC9hPjwv -bGk+CjxsaT48YSBocmVmPSIvaW5kZXgvcmFzcGlzYW5pZV96YW5qYXRpai8wLTYzOCI+PHNwYW4+ -0KDQsNGB0L/QuNGB0LDQvdC40LUg0LfQsNC90Y/RgtC40Lk8L3NwYW4+PC9hPjwvbGk+CjxsaT48 -YSBocmVmPSIvaW5kZXgva2FsZW5kYXJueWpfdWNoZWJueWpfZ3JhZmlrLzAtNjM5Ij48c3Bhbj7Q -mtCw0LvQtdC90LTQsNGA0L3Ri9C5INGD0YfQtdCx0L3Ri9C5INCz0YDQsNGE0LjQujwvc3Bhbj48 -L2E+PC9saT48L3VsPjwvbGk+CjxsaSBjbGFzcz0idVdpdGhTdWJtZW51IGl0ZW0tcGFyZW50Ij48 -YSBocmVmPSIvaW5kZXgvemFvY2hub2Vfb3RkZWxlbmllLzAtNzkiIGNsYXNzPSIiPjxzcGFuPtCX -0LDQvtGH0L3QvtC1INC+0YLQtNC10LvQtdC90LjQtTwvc3Bhbj48L2E+PGVtPis8L2VtPjx1bCBz -dHlsZT0iZGlzcGxheTogbm9uZTsiPgo8bGk+PGEgaHJlZj0iaHR0cDovL3BvbGl0ZWhuaWt1bS1l -bmcucnUvaW5kZXgvMC0zNzMiPjxzcGFuPtCT0YDQsNGE0LjQuiDRg9GH0LXQsdC90L7Qs9C+INC/ -0YDQvtGG0LXRgdGB0LA8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSJodHRwczovL2Nsb3Vk -Lm1haWwucnUvcHVibGljL1k5dkQvQmNYOGJBS1R3IiB0YXJnZXQ9Il9ibGFuayI+PHNwYW4+0JfQ -sNC00LDQvdC40Y8g0LTQu9GPINC00LjRgdGC0LDQvdGG0LjQvtC90L3QvtCz0L4g0L7QsdGD0YfQ -tdC90LjRjzwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9zcGlzb2tfc3R1ZGVu -dG92X3BvX2dydXBwYW0vMC0yNDAiPjxzcGFuPtCh0L/QuNGB0L7QuiDRgdGC0YPQtNC10L3RgtC+ -0LI8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvemltbmphamFfc2Vzc2lqYS8w -LTI1NSI+PHNwYW4+0KDQsNGB0L/QuNGB0LDQvdC40LUg0YHQtdGB0YHQuNC4PC9zcGFuPjwvYT48 -L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L21ldG9kaWNoZXNraWVfcmVrb21lbmRhY2lpLzAtNTU5 -Ij48c3Bhbj7QnNC10YLQvtC00LjRh9C10YHQutC40LUg0YDQtdC60L7QvNC10L3QtNCw0YbQuNC4 -PC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3Z5cHVza25pa3UvMC01NjMiPjxz -cGFuPtCS0YvQv9GD0YHQutC90LjQutGDPC9zcGFuPjwvYT48L2xpPjwvdWw+PC9saT4KPGxpIGNs -YXNzPSJ1V2l0aFN1Ym1lbnUgaXRlbS1wYXJlbnQiPjxhIGhyZWY9Imh0dHA6Ly9wb2xpdGVobmlr -dW0tZW5nLnJ1L2luZGV4L29jaG5vZV9vdGRlbGVuaWUvMC00MDgiIGNsYXNzPSIiPjxzcGFuPtCe -0YfQvdC+0LUg0L7RgtC00LXQu9C10L3QuNC1PC9zcGFuPjwvYT48ZW0+KzwvZW0+PHVsIHN0eWxl -PSJkaXNwbGF5OiBub25lOyI+CjxsaT48YSBocmVmPSIvaW5kZXgva2FsZW5kYXJueWpfdWNoZWJu -eWpfZ3JhZmlrLzAtMTQyIj48c3Bhbj7Qk9GA0LDRhNC40Log0YPRh9C10LHQvdC+0LPQviDQv9GA -0L7RhtC10YHRgdCwPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL25ld3MvemFkYW5pamEv -MjAyMy0wMS0xMS00NjA2Ij48c3Bhbj7Ql9Cw0LTQsNC90LjRjyDQtNC70Y8g0LTQuNGB0YLQsNC9 -0YbQuNC+0L3QvdC+0LPQviDQvtCx0YPRh9C10L3QuNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEg -Y2xhc3M9IiB1TWVudUl0ZW1BIiBocmVmPSJodHRwOi8vcG9saXRlaG5pa3VtLWVuZy5ydS9pbmRl -eC9yYXNwaXNhbmllX3phbmphdGlqLzAtNDA5Ij48c3Bhbj7QoNCw0YHQv9C40YHQsNC90LjQtSDQ -t9Cw0L3Rj9GC0LjQuTwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Imh0dHA6Ly9wb2xpdGVo -bmlrdW0tZW5nLnJ1L2luZGV4L3Jhc3Bpc2FuaWVfZ2lhLzAtNDIxIj48c3Bhbj7QoNCw0YHQv9C4 -0YHQsNC90LjQtSDQk9CY0JA8L3NwYW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvdnlw -dXNrbmlrdS8wLTU1MCI+PHNwYW4+0JLRi9C/0YPRgdC60L3QuNC60YM8L3NwYW4+PC9hPjwvbGk+ -CjxsaT48YSBocmVmPSIvaW5kZXgvcmFzcGlzYW5pZV92cHIvMC02MTgiPjxzcGFuPtCg0LDRgdC/ -0LjRgdCw0L3QuNC1INCS0J/QoDwvc3Bhbj48L2E+PC9saT48L3VsPjwvbGk+CjxsaSBjbGFzcz0i -dVdpdGhTdWJtZW51IGl0ZW0tcGFyZW50Ij48YSBocmVmPSIvaW5kZXgvdm9zcGl0YXRlbG5hamFf -cmFib3RhLzAtMjA0Ij48c3Bhbj7QktC+0YHQv9C40YLQsNGC0LXQu9GM0L3QsNGPINGA0LDQsdC+ -0YLQsDwvc3Bhbj48L2E+PGVtPis8L2VtPjx1bD4KPGxpPjxhIGhyZWY9Ii9pbmRleC9uYXNoaV9k -b3N0aXpoZW5pamEvMC0yMDUiPjxzcGFuPtCd0LDRiNC4INC00L7RgdGC0LjQttC10L3QuNGPPC9z -cGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3NtaV9vX25hcy8wLTUyIj48c3Bhbj7Q -odCc0Jgg0L4g0L3QsNGBPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3N0dWRl -bmNoZXNrYWphX3poaXpuLzAtNjkiPjxzcGFuPtCh0YLRg9C00LXQvdGH0LXRgdC60LDRjyDQttC4 -0LfQvdGMPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iaHR0cDovL3BvbGl0ZWhuaWt1bS1l -bmcucnUvaW5kZXgvMC01MzAiPjxzcGFuPtCa0YDRg9C20LrQuDwvc3Bhbj48L2E+PC9saT4KPGxp -PjxhIGhyZWY9Ii9pbmRleC8wLTU3NSI+PHNwYW4+0KHQvtCy0LXRgiDRgdGC0YPQtNC10L3Rh9C1 -0YHQutC+0LPQviDRgdCw0LzQvtGD0L/RgNCw0LLQu9C10L3QuNGPPC9zcGFuPjwvYT48L2xpPgo8 -bGk+PGEgaHJlZj0iL2luZGV4L3JhYm9jaGllX3Byb2dyYW1teV92b3NwaXRhbmlqYS8wLTYwNCI+ -PHNwYW4+0KDQsNCx0L7Rh9C40LUg0L/RgNC+0LPRgNCw0LzQvNGLINCy0L7RgdC/0LjRgtCw0L3Q -uNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3N0dWRlbmNoZXNraWpfc3Bv -cnRpdm55al9rbHViLzAtNjM1Ij48c3Bhbj7igIvigIvigIvigIvigIvigIvigIvQodGC0YPQtNC1 -0L3Rh9C10YHQutC40Lkg0YHQv9C+0YDRgtC40LLQvdGL0Lkg0LrQu9GD0LE8L3NwYW4+PC9hPjwv -bGk+PC91bD48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4L3ZpcnR1YWxuYWphX2Voa3NrdXJzaWph -LzAtNTc4Ij48c3Bhbj7QktC40YDRgtGD0LDQu9GM0L3QsNGPINGN0LrRgdC60YPRgNGB0LjRjzwv -c3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Imh0dHA6Ly9wb2xpdGVobmlrdW0tZW5nLnJ1L2lu -ZGV4L3Byb2dyYW1tYV92b3NwaXRhbmlqYS8wLTU4NSI+PHNwYW4+0J/RgNC+0LPRgNCw0LzQvNCw -INCy0L7RgdC/0LjRgtCw0L3QuNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0iL2luZGV4 -L2NwZGVoLzAtNTg4Ij48c3Bhbj7QptCf0JTQrTwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9 -Ii9pbmRleC9tYXN0ZXJza2llLzAtNjE3Ij48c3Bhbj7QnNCw0YHRgtC10YDRgdC60LjQtTwvc3Bh -bj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9rb25rdXJzeV9pX29saW1waWFkeS8wLTYx -NiI+PHNwYW4+0JrQvtC90LrRg9GA0YHRiyDQuCDQvtC70LjQvNC/0LjQsNC00Ys8L3NwYW4+PC9h -PjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvYnBvby8wLTYzMSI+PHNwYW4+0JHQn9Ce0J48L3Nw -YW4+PC9hPjwvbGk+CjxsaT48YSBocmVmPSIvaW5kZXgvcHJvZmVzc2lvbmFsaXRldC8wLTYzMyI+ -PHNwYW4+0J/QoNCe0KTQldCh0KHQmNCe0J3QkNCb0JjQotCV0KI8L3NwYW4+PC9hPjwvbGk+Cjxs -aT48YSBocmVmPSIvaW5kZXgvYWtrcmVkaXRhY2lvbm55al9tb25pdG9yaW5nX3Npc3RlbXlfb2Jy -YXpvdmFuaWphXzIwMjMvMC02MzQiPjxzcGFuPtCQ0LrQutGA0LXQtNC40YLQsNGG0LjQvtC90L3R -i9C5INC80L7QvdC40YLQvtGA0LjQvdCzINGB0LjRgdGC0LXQvNGLINC+0LHRgNCw0LfQvtCy0LDQ -vdC40Y8gMjAyMzwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC8wLTQ5NiI+PHNw -YW4+0JDQutC60YDQtdC00LjRgtCw0YbQuNGPPC9zcGFuPjwvYT48L2xpPgo8bGk+PGEgaHJlZj0i -L2luZGV4L29icmtyZWRpdF92X3Nwby8wLTY1MyI+PHNwYW4+0J7QsdGA0LrRgNC10LTQuNGCINCy -INCh0J/Qnjwvc3Bhbj48L2E+PC9saT4KPGxpPjxhIGhyZWY9Ii9pbmRleC9kaXN0YW5jaW9ubm9l -X29idWNoZW5pZS8wLTY1NiI+PHNwYW4+0JTQuNGB0YLQsNC90YbQuNC+0L3QvdC+0LUg0L7QsdGD -0YfQtdC90LjQtTwvc3Bhbj48L2E+PC9saT48L3VsPjwvZGl2PjwhLS0gPC9iYz4gLS0+CjwhLS0g -PC9zYmxvY2tfbm1lbnU+IC0tPgogPGRpdiBjbGFzcz0iY2xyIj48L2Rpdj4KIDwvZGl2PgogPGRp -diBjbGFzcz0iY2xyIj48L2Rpdj4KIDwvbmF2PgogCjwvZGl2Pgo8L2hlYWRlcj48IS0tL1UxQUhF -QURFUjFaLS0+CjwhLS1VMVBST01PMVotLT48IS0tL1UxUFJPTU8xWi0tPgo8ZGl2IGlkPSJjYXNp -bmciPgogPGRpdiBjbGFzcz0id3JhcHBlciI+CiAKIDwhLS0gPG1pZGRsZT4gLS0+CiA8ZGl2IGlk -PSJjb250ZW50Ij4KIDxkaXYgaWQ9ImNvbnQtaSI+CiA8IS0tIDxib2R5PiAtLT48ZGl2IGFsaWdu -PSJjZW50ZXIiPjxhIGhyZWY9Ii8yMDI0L3BvbHRhdnNrYWphXzA2X3NfMDdfcG9fMTNfMTAueGxz -Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjE0cHQiPtCg0LDRgdC/0LjRgdCw0L3QuNC1INC30LDQ -vdGP0YLQuNC5INC90LAg0J/QvtC70YLQsNCy0YHQutC+0LkgMTk8L3NwYW4+PC9hPiZuYnNwOzxz -cGFuIHN0eWxlPSJmb250LXNpemU6IDE4LjY2NjdweDsiPiZuYnNwOzwvc3Bhbj48c3BhbiBzdHls -ZT0idGV4dC1hbGlnbjogLXdlYmtpdC1jZW50ZXI7IGZvbnQtc2l6ZTogMTJweDsiPtC+0YI8c3Bh -biBzdHlsZT0idGV4dC1hbGlnbjogLXdlYmtpdC1jZW50ZXI7IGZvbnQtc2l6ZTogMTJweDsiPiZu -YnNwOzA2LjEwPC9zcGFuPjwvc3Bhbj48L2Rpdj4KCjxkaXYgYWxpZ249ImNlbnRlciI+Jm5ic3A7 -PC9kaXY+Cgo8ZGl2IGFsaWduPSJjZW50ZXIiPjxhIGhyZWY9Ii8yMDI0L3BvbHRhdnNrYWphXzA2 -X3NfMDdfcG9fMTNfa29yci54bHMiPjxzcGFuIHN0eWxlPSJ0ZXh0LWFsaWduOiAtd2Via2l0LWNl -bnRlcjsgZm9udC1zaXplOiAxNHB0OyI+0KDQsNGB0L/QuNGB0LDQvdC40LUg0LfQsNC90Y/RgtC4 -0Lkg0LIg0LPRgNGD0L/Qv9Cw0YUg0L/RgNC+0YTQtdGB0YHQuNC+0L3QsNC70YzQvdC+0Lkg0L/Q -vtC00LPQvtGC0L7QstC60Lgg0L3QsCDQn9C+0LvRgtCw0LLRgdC60L7QuSAxOTwvc3Bhbj48L2E+ -PHNwYW4gc3R5bGU9InRleHQtYWxpZ246IC13ZWJraXQtY2VudGVyOyBmb250LXNpemU6IDE4LjY2 -NjdweDsgbGluZS1oZWlnaHQ6IDI5Ljg2NjdweDsiPiZuYnNwOyZuYnNwOzwvc3Bhbj48c3BhbiBz -dHlsZT0idGV4dC1hbGlnbjogLXdlYmtpdC1jZW50ZXI7IGZvbnQtc2l6ZTogMTJweDsiPtC+0YIg -MDY8L3NwYW4+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTogMTJweDsiPi4xMDwvc3Bhbj48L2Rpdj4K -CjxkaXYgYWxpZ249ImNlbnRlciI+PGJyPgo8YSBocmVmPSIvMjAyNC9HRF8wNl9zXzA3X3BvXzEz -XzEwLTEtLnhscyI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNHB0Ij7QoNCw0YHQv9C40YHQsNC9 -0LjQtSDQt9Cw0L3Rj9GC0LjQuSDQvdCwINCW0LXQu9C10LfQvdC+0LTQvtGA0L7QttC90L7QuSAx -Mzwvc3Bhbj48L2E+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTogMTguNjY2N3B4OyBsaW5lLWhlaWdo -dDogMjkuODY2N3B4OyB0ZXh0LWFsaWduOiAtd2Via2l0LWNlbnRlcjsiPjxhIGhyZWY9Ii8yMDI0 -L0dEXzA2X3NfMDdfcG9fMTNfMTAtMS0ueGxzIj4mbmJzcDs8L2E+Jm5ic3A7PC9zcGFuPjxzcGFu -IHN0eWxlPSJ0ZXh0LWFsaWduOiAtd2Via2l0LWNlbnRlcjsgZm9udC1zaXplOiAxMnB4OyI+0L7R -giAwNi4xMDwvc3Bhbj48L2Rpdj4KCjxkaXYgYWxpZ249ImNlbnRlciI+PGJyPgo8c3BhbiBzdHls -ZT0iZm9udC1zaXplOjE0cHQ7Ij48YSBocmVmPSIvMjAyNC9HRF8wNl9zXzA3X3BvXzEzXzEwX2tv -cnIueGxzIj7QoNCw0YHQv9C40YHQsNC90LjQtSDQt9Cw0L3Rj9GC0LjQuSDQsiDQs9GA0YPQv9C/ -0LDRhSDQv9GA0L7RhNC10YHRgdC40L7QvdCw0LvRjNC90L7QuSDQv9C+0LTQs9C+0YLQvtCy0LrQ -uCDQvdCwINCW0LXQu9C10LfQvdC+0LTQvtGA0L7QttC90L7QuSAxMzwvYT4mbmJzcDsmbmJzcDs8 -L3NwYW4+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTogMTJweDsiPtC+0YIgMDYuMTA8L3NwYW4+PC9k -aXY+PCEtLSA8L2JvZHk+IC0tPgogPC9kaXY+CiA8L2Rpdj4KIDxhc2lkZT4KIDxkaXYgaWQ9InNp -ZGViYXIiPiAKIDxkaXYgY2xhc3M9InNpZGVib3giPgogPGRpdiBjbGFzcz0iaW5uZXIiPgogPGRp -diBzdHlsZT0idGV4dC1hbGlnbjpjZW50ZXI7Ij48c3Bhbj48IS0tPHM1MjEyPi0tPtCf0YDQuNCy -0LXRgtGB0YLQstGD0Y4g0JLQsNGBPCEtLTwvcz4tLT4sIDxiPtCT0L7RgdGC0Yw8L2I+ITwvc3Bh -bj48YnI+CiA8YSB0aXRsZT0i0KDQtdCz0LjRgdGC0YDQsNGG0LjRjyIgaHJlZj0iL3JlZ2lzdGVy -Ij48IS0tPHMzMDg5Pi0tPtCg0LXQs9C40YHRgtGA0LDRhtC40Y88IS0tPC9zPi0tPjwvYT4gfCA8 -YSB0aXRsZT0i0JLRhdC+0LQiIGhyZWY9ImphdmFzY3JpcHQ6OyIgcmVsPSJub2ZvbGxvdyIgb25j -bGljaz0ibG9naW5Qb3B1cEZvcm0oKTsgcmV0dXJuIGZhbHNlOyI+PCEtLTxzMzA4Nz4tLT7QktGF -0L7QtDwhLS08L3M+LS0+PC9hPjwvZGl2PgogPC9kaXY+CiA8ZGl2IGNsYXNzPSJjbHIiPjwvZGl2 -PgogPC9kaXY+CiA8IS0tVTFDTEVGVEVSMVotLT4KPCEtLSA8YmxvY2s1NjE2PiAtLT4KPGRpdiBj -bGFzcz0ic2lkZWJveCI+PGRpdiBjbGFzcz0ic2lkZXRpdGxlIj48c3Bhbj48IS0tIDxidD4gLS0+ -MjAyNCDQs9C+0LQ8IS0tIDwvYnQ+IC0tPjwvc3Bhbj48L2Rpdj4KPGRpdiBjbGFzcz0iaW5uZXIi -Pgo8IS0tIDxiYz4gLS0+PHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Imh0 -dHBzOi8v0YHQtdC80YzRjzIwMjQu0YDRhC8iPjxpbWcgYWx0PSIiIHNyYz0iLzIwMjQvZ29kX3Nl -bWkuanBnIiBzdHlsZT0id2lkdGg6IDk1JTsiPjwvYT48L3A+PCEtLSA8L2JjPiAtLT4gCjwvZGl2 -Pgo8ZGl2IGNsYXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrNTYxNj4gLS0+Cgo8 -IS0tIDxibG9jazUyNzU+IC0tPgo8ZGl2IGNsYXNzPSJzaWRlYm94Ij48ZGl2IGNsYXNzPSJzaWRl -dGl0bGUiPjxzcGFuPjwhLS0gPGJ0PiAtLT7Qk9C+0YHRg9GB0LvRg9Cz0Lg8IS0tIDwvYnQ+IC0t -Pjwvc3Bhbj48L2Rpdj4KPGRpdiBjbGFzcz0iaW5uZXIiPgo8IS0tIDxiYz4gLS0+PGgyPjxhIGhy -ZWY9Imh0dHBzOi8vYXBwcy5ydXN0b3JlLnJ1L2FwcC9ydS5nb3N1c2x1Z2kucG9zIiB0YXJnZXQ9 -Il9ibGFuayI+wqvQk9C+0YHRg9GB0LvRg9Cz0LguINCg0LXRiNCw0LXQvCDQstC80LXRgdGC0LXC -uzwvYT48L2gyPgoKPHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Imh0dHBz -Oi8vYXBwcy5ydXN0b3JlLnJ1L2FwcC9ydS5nb3N1c2x1Z2kucG9zIj48aW1nIGFsdD0iIiBzcmM9 -Ii8yMDI0L1J1U3RvcmUucG5nIiBzdHlsZT0id2lkdGg6IDIwJTsiPjwvYT48YSBocmVmPSJodHRw -czovL3BsYXkuZ29vZ2xlLmNvbS9zdG9yZS9hcHBzL2RldGFpbHM/aWQ9cnUuZ29zdXNsdWdpLnBv -cyZhbXA7aGw9cnUiPjxpbWcgYWx0PSIiIHNyYz0iLzIwMjQvQW5kcm9pZC5wbmciIHN0eWxlPSJ3 -aWR0aDogMjAlOyI+PC9hPiA8YSBocmVmPSJodHRwczovL2FwcHMuYXBwbGUuY29tL3J1L2FwcC8l -RDAlQjMlRDAlQkUlRDElODElRDElODMlRDElODElRDAlQkIlRDElODMlRDAlQjMlRDAlQjgtJUQx -JTgwJUQwJUI1JUQxJTg4JUQwJUIwJUQwJUI1JUQwJUJDLSVEMCVCMiVEMCVCQyVEMCVCNSVEMSU4 -MSVEMSU4MiVEMCVCNS9pZDE1MTY0NDgwMTUiPjxpbWcgYWx0PSIiIGNsYXNzPSIiIHNyYz0iLzIw -MjQvQXBwbGUucG5nIiBzdHlsZT0id2lkdGg6IDIwJTsiPjwvYT48L3A+PCEtLSA8L2JjPiAtLT4g -CjwvZGl2Pgo8ZGl2IGNsYXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrNTI3NT4g -LS0+Cgo8IS0tIDxibG9jazcwNzM+IC0tPgo8ZGl2IGNsYXNzPSJzaWRlYm94Ij48ZGl2IGNsYXNz -PSJzaWRldGl0bGUiPjxzcGFuPjwhLS0gPGJ0PiAtLT7Qn9C+0LTRgNCw0LfQtNC10LvQtdC90LjR -jzwhLS0gPC9idD4gLS0+PC9zcGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJpbm5lciI+CjwhLS0gPGJj -PiAtLT48cCBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iL2luZGV4L3Byb2Zz -b2p1ei8wLTExMiI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNnB4OyI+0J/RgNC+0YTRgdC+0Y7Q -tzwvc3Bhbj48L2E+PC9wPgo8cCBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0i -L2FiaXR1ciI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNnB4OyI+0JDQsdC40YLRg9GA0LjQtdC9 -0YLRgzwvc3Bhbj48L2E+PC9wPgo8cCBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJl -Zj0iL2luZGV4L3N0cmFuaWNhX2p1cmlzdGEvMC02NyI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTox -NnB4OyI+0KHRgtGA0LDQvdC40YbQsCDRjtGA0LjRgdGC0LA8L3NwYW4+PC9hPjwvcD4KPHAgc3R5 -bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Ii9pbmRleC8wLTQ0NyI+PHNwYW4gc3R5 -bGU9ImZvbnQtc2l6ZToxNnB4OyI+0J7RgtC00LXQuyDQutCw0LTRgNC+0LI8L3NwYW4+PC9hPjwv -cD4KPHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Ii9pbmRleC9wc2lraG9s -b2dpY2hlc2thamFfc2x1emhiYS8wLTUyNCI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNnB4OyI+ -0J/RgdC40YXQvtC70L7Qs9C40YfQtdGB0LrQsNGPINGB0LvRg9C20LHQsDwvc3Bhbj48L2E+PC9w -Pgo8cCBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iL2luZGV4L21lZF9kZWph -dGVsbm9zdC8wLTUyOSI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNnB4OyI+0JzQtdC0LiDQtNC1 -0Y/RgtC10LvRjNC90L7RgdGC0Yw8L3NwYW4+PC9hPjwvcD4KPHAgc3R5bGU9InRleHQtYWxpZ246 -IGNlbnRlcjsiPjxhIGhyZWY9Imh0dHBzOi8vZGlzay55YW5kZXgucnUvZC9uaG53eXFFOHF5Tklw -USI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNnB4OyI+0J/RgNC+0LXQutGCIMKr0JHQtdC3INGB -0YDQvtC60LAg0LTQsNCy0L3QvtGB0YLQuMK7PC9zcGFuPjwvYT48L3A+CjxwIHN0eWxlPSJ0ZXh0 -LWFsaWduOiBjZW50ZXI7Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjE2cHg7Ij7Qk9C+0YDRj9GH -0LDRjyDQu9C40L3QuNGPJm5ic3A70L/QviDQstC+0L/RgNC+0YHQsNC8INC/0YDQuNC10LzQsCDQ -uCDQvtCx0YPRh9C10L3QuNGPINC70LjRhiDRgSDQvtCz0YDQsNC90LjRh9C10L3QvdGL0LzQuCDQ -stC+0LfQvNC+0LbQvdC+0YHRgtGP0LzQuCDQt9C00L7RgNC+0LLRjNGPINC4INC40L3QstCw0LvQ -uNC00L3QvtGB0YLRjNGOIDxhIGhyZWY9InRlbDorNzg0NTM3NzU3NjIiPiZuYnNwOzgtODQ1My03 -Ny01Ny02MjwvYT48L3NwYW4+PC9wPjwhLS0gPC9iYz4gLS0+IAo8L2Rpdj4KPGRpdiBjbGFzcz0i -Y2xyIj48L2Rpdj4KPC9kaXY+CjwhLS0gPC9ibG9jazcwNzM+IC0tPgoKPCEtLSA8YmxvY2szMjUw -PiAtLT4KPGRpdiBjbGFzcz0ic2lkZWJveCI+PGRpdiBjbGFzcz0ic2lkZXRpdGxlIj48c3Bhbj48 -IS0tIDxidD4gLS0+0JfQsNGJ0LjRgtC40Lwg0KHQktCe0LjRhTwhLS0gPC9idD4gLS0+PC9zcGFu -PjwvZGl2Pgo8ZGl2IGNsYXNzPSJpbm5lciI+CjwhLS0gPGJjPiAtLT48cCBzdHlsZT0idGV4dC1h -bGlnbjogY2VudGVyOyI+CjxhIGNsYXNzPSJ1bGlnaHRib3giIGhyZWY9Ii8yMDI0LzA1XzMwLzU4 -LmpwZyIgc3R5bGU9ImZvbnQtc2l6ZTogMTJwdDsiIHRhcmdldD0iX2JsYW5rIiB0aXRsZT0i0J3Q -sNC20LzQuNGC0LUsINC00LvRjyDQv9GA0L7RgdC80L7RgtGA0LAg0LIg0L/QvtC70L3QvtC8INGA -0LDQt9C80LXRgNC1Li4uIj48aW1nIGFsaWduPSIiIGFsdD0iIiBzcmM9Ii8yMDI0LzA1XzMwLzU4 -LmpwZyIgc3R5bGU9InBhZGRpbmc6MTtib3JkZXI6IDBweDsgaGVpZ2h0OiAzMDBweDsiPjwvYT48 -L3A+PCEtLSA8L2JjPiAtLT4gCjwvZGl2Pgo8ZGl2IGNsYXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4K -PCEtLSA8L2Jsb2NrMzI1MD4gLS0+Cgo8IS0tIDxibG9jazkzNzY+IC0tPgo8ZGl2IGNsYXNzPSJz -aWRlYm94Ij48ZGl2IGNsYXNzPSJzaWRldGl0bGUiPjxzcGFuPjwhLS0gPGJ0PiAtLT7QntCx0YrR -j9GB0L3Rj9C10Lwu0YDRhDwhLS0gPC9idD4gLS0+PC9zcGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJp -bm5lciI+CjwhLS0gPGJjPiAtLT48cD48YSBocmVmPSJodHRwczovL9C+0LHRitGP0YHQvdGP0LXQ -vC7RgNGELyIgdGFyZ2V0PSJfYmxhbmsiPjxpbWcgYWx0PSIiIHNyYz0iLzIwMjIvMDNfMTcvODQu -anBnIiBzdHlsZT0ibWFyZ2luOiAwIDAgMCAxMHB4OyB3aWR0aDogOTAlOyI+PC9hPjwhLS0gPC9i -Yz4gLS0+IAo8L3A+PC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2Pgo8IS0tIDwv -YmxvY2s5Mzc2PiAtLT4KCjwhLS0gPGJsb2NrNzIxNT4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3gi -PjxkaXYgY2xhc3M9InNpZGV0aXRsZSI+PHNwYW4+PCEtLSA8YnQ+IC0tPtCf0YDQtdC80LjRjyAj -0JzQq9CS0JzQldCh0KLQlTwhLS0gPC9idD4gLS0+PC9zcGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJp -bm5lciI+CjwhLS0gPGJjPiAtLT48YSBjbGFzcz0idWxpZ2h0Ym94IiBocmVmPSIgLzIwMjIvMDVf -MzEvNDYucG5nIiBzdHlsZT0iZm9udC1zaXplOiAxMnB0OyIgdGFyZ2V0PSJfYmxhbmsiIHRpdGxl -PSLQndCw0LbQvNC40YLQtSwg0LTQu9GPINC/0YDQvtGB0LzQvtGC0YDQsCDQsiDQv9C+0LvQvdC+ -0Lwg0YDQsNC30LzQtdGA0LUuLi4iPjxpbWcgYWxpZ249IiIgYWx0PSIiIHNyYz0iIC8yMDIyLzA1 -XzMxLzQ2LnBuZyIgc3R5bGU9Im1hcmdpbjowIDAgMCAxMHB4O3BhZGRpbmc6MTtib3JkZXI6IDBw -eDsgd2lkdGg6IDEwMCU7Ij48L2E+PCEtLSA8L2JjPiAtLT4gCjwvZGl2Pgo8ZGl2IGNsYXNzPSJj -bHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrNzIxNT4gLS0+Cgo8IS0tIDxibG9jazI0ODY+ -IC0tPgo8ZGl2IGNsYXNzPSJzaWRlYm94Ij48ZGl2IGNsYXNzPSJzaWRldGl0bGUiPjxzcGFuPjwh -LS0gPGJ0PiAtLT7QntCx0YrRj9Cy0LvQtdC90LjQtTwhLS0gPC9idD4gLS0+PC9zcGFuPjwvZGl2 -Pgo8ZGl2IGNsYXNzPSJpbm5lciI+CjwhLS0gPGJjPiAtLT48YSBjbGFzcz0idWxpZ2h0Ym94IiBo -cmVmPSIvMjAyNC9qdW5vc2hlc2thamEtYXZ0b3Noa29sYS5qcGciIHN0eWxlPSJmb250LXNpemU6 -IDEycHQ7IiB0YXJnZXQ9Il9ibGFuayIgdGl0bGU9ItCd0LDQttC80LjRgtC1LCDQtNC70Y8g0L/R -gNC+0YHQvNC+0YLRgNCwINCyINC/0L7Qu9C90L7QvCDRgNCw0LfQvNC10YDQtS4uLiI+PGltZyBh -bGlnbj0iIiBhbHQ9IiIgc3JjPSIvMjAyNC9qdW5vc2hlc2thamEtYXZ0b3Noa29sYS5qcGciIHN0 -eWxlPSJwYWRkaW5nOjE7Ym9yZGVyOiAwcHg7IHdpZHRoOiAxMDAlOyI+PC9hPjwhLS0gPC9iYz4g -LS0+IAo8L2Rpdj4KPGRpdiBjbGFzcz0iY2xyIj48L2Rpdj4KPC9kaXY+CjwhLS0gPC9ibG9jazI0 -ODY+IC0tPgoKPCEtLSA8YmxvY2sxMDA2Mz4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3giPjxkaXYg -Y2xhc3M9InNpZGV0aXRsZSI+PHNwYW4+PCEtLSA8YnQ+IC0tPtCi0YDRg9C00L7Rg9GB0YLRgNC+ -0LnRgdGC0LLQvjwhLS0gPC9idD4gLS0+PC9zcGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJpbm5lciI+ -CjwhLS0gPGJjPiAtLT48aDMgYWxpZ249ImNlbnRlciI+PGEgaHJlZj0iL2luZGV4L3RydWRvdXN0 -cm9qc3R2by8wLTU4OSI+0KbQtdC90YLRgCDQutCw0YDRjNC10YDRizwvYT48L2gzPgo8aDQgYWxp -Z249ImNlbnRlciI+PGEgaHJlZj0iL3RydWQvTWlucHJvc3Zlc2hoZW5peWFfUm9zc2lpLnBkZiI+ -0J/QvtC40YHQuiDRgNCw0LHQvtGC0Ysg0YEg0L/QvtC80L7RidGM0Y4g0J/QvtGA0YLQsNC70LAg -wqvQoNCw0LHQvtGC0LAg0LIg0KDQvtGB0YHQuNC4wrs8L2E+PC9oND4KPGg0IGFsaWduPSJjZW50 -ZXIiPjxhIGhyZWY9Ii9pbmRleC9hZ3JlZ2F0b3J5X3Zha2Fuc2lqX2RsamFfc3R1ZGVudG92X2lf -dnlwdXNrbmlrb3YvMC01OTkiPtCQ0LPRgNC10LPQsNGC0L7RgNGLINCy0LDQutCw0L3RgdC40Lkg -0LTQu9GPINGB0YLRg9C00LXQvdGC0L7QsiDQuCDQstGL0L/Rg9GB0LrQvdC40LrQvtCyPC9hPjwv -aDQ+CjxoNCBhbGlnbj0iY2VudGVyIj48YSBocmVmPSJodHRwczovL2pvYmthZHJvdi5ydS92YWNh -bmNpZXMvcmVnaW9uL3NhcmF0b3Zza2FpYS1vYmxhc3RfNjIiPtCS0LDQutCw0L3RgdC40Lgg0Lgg -0L/RgNCw0LrRgtC40LrQsCDQptC10L3RgtGA0LAg0LfQsNC90Y/RgtC+0YHRgtC4INCh0LDRgNCw -0YLQvtCy0YHQutC+0Lkg0L7QsdC70LDRgdGC0LggPC9hPjwvaDQ+PCEtLSA8L2JjPiAtLT4gCjwv -ZGl2Pgo8ZGl2IGNsYXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrMTAwNjM+IC0t -PgoKPCEtLSA8YmxvY2s2NDg4PiAtLT4KPGRpdiBjbGFzcz0ic2lkZWJveCI+PGRpdiBjbGFzcz0i -c2lkZXRpdGxlIj48c3Bhbj48IS0tIDxidD4gLS0+0JrQvtC90YLQsNC60YLRiyDQk9CY0KI8IS0t -IDwvYnQ+IC0tPjwvc3Bhbj48L2Rpdj4KPGRpdiBjbGFzcz0iaW5uZXIiPgo8IS0tIDxiYz4gLS0+ -PHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGNsYXNzPSJ1bGlnaHRib3giIHN0eWxl -PSJmb250LXNpemU6IDEycHQ7IiB0aXRsZT0i0J3QsNC20LzQuNGC0LUsINC00LvRjyDQv9GA0L7R -gdC80L7RgtGA0LAg0LIg0L/QvtC70L3QvtC8INGA0LDQt9C80LXRgNC1Li4uIiBocmVmPSIvMjAy -NC9rb250YWt0eV9naXQuanBnIiB0YXJnZXQ9Il9ibGFuayI+PGltZyBzdHlsZT0ibWFyZ2luOiAw -IDAgMCAxMHB4OyBwYWRkaW5nOiAxOyBib3JkZXI6IDBweDsgd2lkdGg6IDkwJTsiIHNyYz0iLzIw -MjQva29udGFrdHlfZ2l0LmpwZyIgYWx0PSIiIGFsaWduPSIiPjwvYT48L3A+PCEtLSA8L2JjPiAt -LT4gCjwvZGl2Pgo8ZGl2IGNsYXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrNjQ4 -OD4gLS0+Cgo8IS0tIDxibG9jazM2Nj4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3giPjxkaXYgY2xh -c3M9InNpZGV0aXRsZSI+PHNwYW4+PCEtLSA8YnQ+IC0tPtCd0LXQt9Cw0LLQuNGB0LjQvNCw0Y8g -0L7RhtC10L3QutCwPCEtLSA8L2J0PiAtLT48L3NwYW4+PC9kaXY+CjxkaXYgY2xhc3M9ImlubmVy -Ij4KPCEtLSA8YmM+IC0tPjxwIHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij48YSBocmVmPSJo -dHRwczovL2RvY3MuZ29vZ2xlLmNvbS9mb3Jtcy9kL2UvMUZBSXBRTFNkTmJDanlYVVN3VGtkUDhO -M0tDYU9NYzFvV3ZuQnZ3MTBnT2tXbWoyVi1maURXQXcvdmlld2Zvcm0iPjxzcGFuIHN0eWxlPSJm -b250LXNpemU6IDE0cHg7Ij7QkNC90LrQtdGC0LAg0LTQu9GPINC/0YDQvtCy0LXQtNC10L3QuNGP -INC+0L/RgNC+0YHQsCDQv9C+0LvRg9GH0LDRgtC10LvQtdC5INGD0YHQu9GD0LMg0LIg0YDQsNC8 -0LrQsNGFINC90LXQt9Cw0LLQuNGB0LjQvNC+0Lkg0L7RhtC10L3QutC4INC60LDRh9C10YHRgtCy -0LAg0YPRgdC70L7QstC40Lkg0L7RgdGD0YnQtdGB0YLQstC70LXQvdC40Y8g0L7QsdGA0LDQt9C+ -0LLQsNGC0LXQu9GM0L3QvtC5INC00LXRj9GC0LXQu9GM0L3QvtGB0YLQuDwvc3Bhbj48L2E+PC9w -Pgo8cCBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iaHR0cHM6Ly9kb2NzLmdv -b2dsZS5jb20vZm9ybXMvZC9lLzFGQUlwUUxTZmlYODhySGd5RDdudWRiR0wyanJTT2hJQk90MXcy -eFA2WGZWVlROaHlVblpNamp3L3ZpZXdmb3JtIj48c3BhbiBzdHlsZT0iZm9udC1zaXplOiAxNHB4 -OyI+0JDQvdC60LXRgtCwINC+0YbQtdC90LrQuCDRg9C00L7QstC70LXRgtCy0L7RgNC10L3QvdC+ -0YHRgtC4INGA0LDQsdC+0YLQvtC00LDRgtC10LvRjyDQutCw0YfQtdGB0YLQstC+0Lwg0L/QvtC0 -0LPQvtGC0L7QstC60Lgg0LLRi9C/0YPRgdC60L3QuNC60L7QsiDRgtC10YXQvdC40LrRg9C80LA8 -L3NwYW4+PC9hPjwvcD4KPHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Imh0 -dHBzOi8vZG9jcy5nb29nbGUuY29tL2Zvcm1zL2QvZS8xRkFJcFFMU2VVSjNURWRHSUV1ekJDSVpR -Snl1VXZ0TjNoWHNUMzFFVkVIb1VwVnlzYVFZRGgyQS92aWV3Zm9ybSI+PHNwYW4gc3R5bGU9ImZv -bnQtc2l6ZTogMTRweDsiPtCQ0L3QutC10YLQsCDQv9C10LTQsNCz0L7Qs9C40YfQtdGB0LrQvtCz -0L4g0YDQsNCx0L7RgtC90LjQutCwINC00LvRjyDQvtGB0YPRidC10YHRgtCy0LvQtdC90LjRjyDQ -stC90YPRgtGA0LXQvdC90LXQuSDQvtGG0LXQvdC60Lgg0LrQsNGH0LXRgdGC0LLQsCDQvtCx0YDQ -sNC30L7QstCw0L3QuNGPPC9zcGFuPjwvYT48L3A+CjxwIHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50 -ZXI7Ij48YSBocmVmPSJodHRwczovL2RvY3MuZ29vZ2xlLmNvbS9mb3Jtcy9kL2UvMUZBSXBRTFNk -QzRPb3U1dkh6Nk5BcGh1RHM2ZXo0QVN0QVIxNHl0NmkwSWozUjYzUnB3N2dlVFEvdmlld2Zvcm0i -PjxzcGFuIHN0eWxlPSJmb250LXNpemU6IDE0cHg7Ij7QkNC90LrQtdGC0LAg0L7QsdGD0YfQsNGO -0YnQtdCz0L7RgdGPPC9zcGFuPjwvYT48L3A+PCEtLSA8L2JjPiAtLT4gCjwvZGl2Pgo8ZGl2IGNs -YXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrMzY2PiAtLT4KCjwhLS0gPGJsb2Nr -NjczMD4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3giPjxkaXYgY2xhc3M9InNpZGV0aXRsZSI+PHNw -YW4+PCEtLSA8YnQ+IC0tPtCS0YHQtSDQtNC70Y8g0J/QvtCx0LXQtNGLITwhLS0gPC9idD4gLS0+ -PC9zcGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJpbm5lciI+CjwhLS0gPGJjPiAtLT48cCBzdHlsZT0i -dGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iaHR0cHM6Ly9wb2JlZGEub25mLnJ1LyI+PGlt -ZyBhbHQ9IiIgc3JjPSIvMjAyMi8wNl8yOC81MC5qcGciIHN0eWxlPSJ3aWR0aDogOTAlOyI+PC9h -PjwvcD48IS0tIDwvYmM+IC0tPiAKPC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2 -Pgo8IS0tIDwvYmxvY2s2NzMwPiAtLT4KCjwhLS0gPGJsb2NrNzYyNz4gLS0+CjxkaXYgY2xhc3M9 -InNpZGVib3giPjxkaXYgY2xhc3M9InNpZGV0aXRsZSI+PHNwYW4+PCEtLSA8YnQ+IC0tPtCf0YDQ -vtC10LrRgiDQlNGA0YPQs9C+0LUg0LTQtdC70L48IS0tIDwvYnQ+IC0tPjwvc3Bhbj48L2Rpdj4K -PGRpdiBjbGFzcz0iaW5uZXIiPgo8IS0tIDxiYz4gLS0+PHAgc3R5bGU9InRleHQtYWxpZ246IGNl -bnRlcjsiPjxhIGhyZWY9Imh0dHBzOi8vdHJrLm1haWwucnUvYy9hdTgxZzE/bXRfY2FtcGFpbmc9 -REQmYW1wO210X2Fkc2V0PXRzdXImYW1wO210X25ldHdvcms9d2Vic2l0ZSZhbXA7bXRfY3JlYXRp -dmU9YmFubmVyI210X2NhbXBhaW5nPUREJmFtcDttdF9hZHNldD10c3VyJmFtcDttdF9uZXR3b3Jr -PXdlYnNpdGUmYW1wO210X2NyZWF0aXZlPWJhbm5lciI+PGltZyBhbHQ9IiIgc3JjPSIvMjAyMS8z -MjB4NTAuZ2lmIiBzdHlsZT0id2lkdGg6IDEwMCU7Ij48L2E+PC9wPjwhLS0gPC9iYz4gLS0+IAo8 -L2Rpdj4KPGRpdiBjbGFzcz0iY2xyIj48L2Rpdj4KPC9kaXY+CjwhLS0gPC9ibG9jazc2Mjc+IC0t -PgoKPCEtLSA8YmxvY2s5ODA1PiAtLT4KPGRpdiBjbGFzcz0ic2lkZWJveCI+PGRpdiBjbGFzcz0i -c2lkZXRpdGxlIj48c3Bhbj48IS0tIDxidD4gLS0+0J/QsNC80Y/RgtC60Lg8IS0tIDwvYnQ+IC0t -Pjwvc3Bhbj48L2Rpdj4KPGRpdiBjbGFzcz0iaW5uZXIiPgo8IS0tIDxiYz4gLS0+PHN0eWxlPi5m -b3RvcmFtYTE3MjgyMzE4MzM4OTEgLmZvdG9yYW1hX19uYXYtLXRodW1icyAuZm90b3JhbWFfX25h -dl9fZnJhbWV7CnBhZGRpbmc6MnB4OwpoZWlnaHQ6NDhweH0KLmZvdG9yYW1hMTcyODIzMTgzMzg5 -MSAuZm90b3JhbWFfX3RodW1iLWJvcmRlcnsKaGVpZ2h0OjQ0cHg7CmJvcmRlci13aWR0aDoycHg7 -Cm1hcmdpbi10b3A6MnB4fTwvc3R5bGU+PGRpdiBjbGFzcz0iZm90b3JhbWEtLWhpZGRlbiI+PC9k -aXY+PGRpdiBjbGFzcz0iZm90b3JhbWEgZm90b3JhbWExNzI4MjMxODMzODkxIiBkYXRhLWFsbG93 -ZnVsbHNjcmVlbj0idHJ1ZSIgZGF0YS1hdXRvcGxheT0idHJ1ZSIgZGF0YS1sb29wPSJ0cnVlIiBk -YXRhLW5hdj0idGh1bWJzIiBkYXRhLXJhdGlvPSI4MDAvNjAwIiBkYXRhLXRodW1iaGVpZ2h0PSI0 -OCIgZGF0YS13aWR0aD0iMzUwIiBzdHlsZT0ibWFyZ2luOiAwIGF1dG87IHdpZHRoOjkwJTsiPjxk -aXYgY2xhc3M9ImZvdG9yYW1hX193cmFwIGZvdG9yYW1hX193cmFwLS1jc3MzIGZvdG9yYW1hX193 -cmFwLS1zbGlkZSBmb3RvcmFtYV9fd3JhcC0tdG9nZ2xlLWFycm93cyIgc3R5bGU9IndpZHRoOiAz -NTBweDsgbWluLXdpZHRoOiAwcHg7IG1heC13aWR0aDogMTAwJTsiPjxkaXYgY2xhc3M9ImZvdG9y -YW1hX19zdGFnZSIgc3R5bGU9IndpZHRoOiAyODBweDsgaGVpZ2h0OiAyMTBweDsiPjxkaXYgY2xh -c3M9ImZvdG9yYW1hX19mdWxsc2NyZWVuLWljb24iIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24i -PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19zdGFnZV9fc2hhZnQiIHN0eWxlPSJ0cmFuc2l0 -aW9uLWR1cmF0aW9uOiAzMDBtczsgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgtMjgycHgsIDBweCwg -MHB4KTsgd2lkdGg6IDI4MHB4OyBtYXJnaW4tbGVmdDogMHB4OyI+PGRpdiBjbGFzcz0iZm90b3Jh -bWFfX3N0YWdlX19mcmFtZSBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyIg -c3R5bGU9ImxlZnQ6IC0yODJweDsiPjxpbWcgc3JjPSIvMjAyMC8xMl8xMC85OC5qcGciIGNsYXNz -PSJmb3RvcmFtYV9faW1nIiBzdHlsZT0id2lkdGg6IDI4MHB4OyBoZWlnaHQ6IDE5OXB4OyBsZWZ0 -OiAwcHg7IHRvcDogNXB4OyI+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3N0YWdlX19mcmFt -ZSBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyBmb3RvcmFtYV9fYWN0aXZl -IiBzdHlsZT0ibGVmdDogMHB4OyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzk5LmpwZyIgY2xhc3M9 -ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDogMjgwcHg7IGhlaWdodDogMTk0cHg7IGxlZnQ6 -IDBweDsgdG9wOiA4cHg7Ij48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fc3RhZ2VfX2ZyYW1l -IGZvdG9yYW1hX19sb2FkZWQgZm90b3JhbWFfX2xvYWRlZC0taW1nIiBzdHlsZT0ibGVmdDogMjgy -cHg7Ij48aW1nIHNyYz0iLzIwMjAvMTJfMTAvMTAwLmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWci -IHN0eWxlPSJ3aWR0aDogMjgwcHg7IGhlaWdodDogMTk4cHg7IGxlZnQ6IDBweDsgdG9wOiA2cHg7 -Ij48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fc3RhZ2VfX2ZyYW1lIiBzdHlsZT0ibGVmdDog -NTY0cHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fYXJyIGZvdG9yYW1hX19h -cnItLXByZXYiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iPjwvZGl2PjxkaXYgY2xhc3M9ImZv -dG9yYW1hX19hcnIgZm90b3JhbWFfX2Fyci0tbmV4dCIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRv -biI+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3ZpZGVvLWNsb3NlIj48L2Rpdj48L2Rpdj48 -ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2LXdyYXAiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXYg -Zm90b3JhbWFfX25hdi0tdGh1bWJzIGZvdG9yYW1hX19zaGFkb3dzLS1sZWZ0IGZvdG9yYW1hX19z -aGFkb3dzLS1yaWdodCIgc3R5bGU9IndpZHRoOiAyODBweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1h -X19uYXZfX3NoYWZ0IGZvdG9yYW1hX19ncmFiIiBzdHlsZT0idHJhbnNpdGlvbi1kdXJhdGlvbjog -MzMwbXM7IHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoLTEyMTJweCwgMHB4LCAwcHgpOyI+PGRpdiBj -bGFzcz0iZm90b3JhbWFfX3RodW1iLWJvcmRlciIgc3R5bGU9InRyYW5zaXRpb24tZHVyYXRpb246 -IDM2MG1zOyB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDEzMjBweCwgMHB4LCAwcHgpOyB3aWR0aDog -NjBweDsiPjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19u -YXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRo -OiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIGZvdG9yYW1hX19sb2FkZWQgZm90 -b3JhbWFfX2xvYWRlZC0taW1nIj48aW1nIHNyYz0iLzIwMjAvMTJfMTAvMjIuanBnIiBjbGFzcz0i -Zm90b3JhbWFfX2ltZyIgc3R5bGU9IndpZHRoOiA2NHB4OyBoZWlnaHQ6IDU0cHg7IGxlZnQ6IDBw -eDsgdG9wOiAtM3B4OyI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJh -bWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9u -IiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIgZm90b3Jh -bWFfX2xvYWRlZCBmb3RvcmFtYV9fbG9hZGVkLS1pbWciPjxpbWcgc3JjPSIvMjAyMC8xMl8xMC8y -My5qcGciIGNsYXNzPSJmb3RvcmFtYV9faW1nIiBzdHlsZT0id2lkdGg6IDY4cHg7IGhlaWdodDog -NDhweDsgbGVmdDogLTJweDsgdG9wOiAwcHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3Rv -cmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIw -IiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1h -X190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9 -Ii8yMDIwLzEyXzEwLzI0LmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDog -NjRweDsgaGVpZ2h0OiA3MXB4OyBsZWZ0OiAwcHg7IHRvcDogLTEycHg7Ij48L2Rpdj48L2Rpdj48 -ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1 -bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYg -Y2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQt -LWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzI1LmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWci -IHN0eWxlPSJ3aWR0aDogNjRweDsgaGVpZ2h0OiA4OXB4OyBsZWZ0OiAwcHg7IHRvcDogLTIxcHg7 -Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9f -bmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0 -aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZv -dG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzI2LmpwZyIgY2xhc3M9 -ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDogNjhweDsgaGVpZ2h0OiA0OHB4OyBsZWZ0OiAt -MnB4OyB0b3A6IDBweDsiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2Zy -YW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRv -biIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIGZvdG9y -YW1hX19sb2FkZWQgZm90b3JhbWFfX2xvYWRlZC0taW1nIj48aW1nIHNyYz0iLzIwMjAvMTJfMTAv -MjcuanBnIiBjbGFzcz0iZm90b3JhbWFfX2ltZyIgc3R5bGU9IndpZHRoOiA2OHB4OyBoZWlnaHQ6 -IDQ4cHg7IGxlZnQ6IC0ycHg7IHRvcDogMHB4OyI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90 -b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0i -MCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFt -YV9fdGh1bWIgZm90b3JhbWFfX2xvYWRlZCBmb3RvcmFtYV9fbG9hZGVkLS1pbWciPjxpbWcgc3Jj -PSIvMjAyMC8xMl8xMC8yOC5qcGciIGNsYXNzPSJmb3RvcmFtYV9faW1nIiBzdHlsZT0id2lkdGg6 -IDY4cHg7IGhlaWdodDogNDhweDsgbGVmdDogLTJweDsgdG9wOiAwcHg7Ij48L2Rpdj48L2Rpdj48 -ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1 -bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYg -Y2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQt -LWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzI5LmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWci -IHN0eWxlPSJ3aWR0aDogNjhweDsgaGVpZ2h0OiA0OHB4OyBsZWZ0OiAtMnB4OyB0b3A6IDBweDsi -PjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19u -YXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRo -OiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIGZvdG9yYW1hX19sb2FkZWQgZm90 -b3JhbWFfX2xvYWRlZC0taW1nIj48aW1nIHNyYz0iLzIwMjAvMTJfMTAvMzAuanBnIiBjbGFzcz0i -Zm90b3JhbWFfX2ltZyIgc3R5bGU9IndpZHRoOiA2OHB4OyBoZWlnaHQ6IDQ4cHg7IGxlZnQ6IC0y -cHg7IHRvcDogMHB4OyI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJh -bWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9u -IiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIgZm90b3Jh -bWFfX2xvYWRlZCBmb3RvcmFtYV9fbG9hZGVkLS1pbWciPjxpbWcgc3JjPSIvMjAyMC8xMl8xMC8z -MS5qcGciIGNsYXNzPSJmb3RvcmFtYV9faW1nIiBzdHlsZT0id2lkdGg6IDY2cHg7IGhlaWdodDog -NDhweDsgbGVmdDogLTFweDsgdG9wOiAwcHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3Rv -cmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIw -IiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1h -X190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9 -Ii8yMDIwLzEyXzEwLzMyLmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDog -NjhweDsgaGVpZ2h0OiA0OHB4OyBsZWZ0OiAtMnB4OyB0b3A6IDBweDsiPjwvZGl2PjwvZGl2Pjxk -aXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVt -YiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBj -bGFzcz0iZm90b3JhbWFfX3RodW1iIGZvdG9yYW1hX19sb2FkZWQgZm90b3JhbWFfX2xvYWRlZC0t -aW1nIj48aW1nIHNyYz0iLzIwMjAvMTJfMTAvOTEuanBnIiBjbGFzcz0iZm90b3JhbWFfX2ltZyIg -c3R5bGU9IndpZHRoOiA2OHB4OyBoZWlnaHQ6IDQ4cHg7IGxlZnQ6IC0ycHg7IHRvcDogMHB4OyI+ -PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25h -dl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6 -IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIgZm90b3JhbWFfX2xvYWRlZCBmb3Rv -cmFtYV9fbG9hZGVkLS1pbWciPjxpbWcgc3JjPSIvMjAyMC8xMl8xMC85Mi5qcGciIGNsYXNzPSJm -b3RvcmFtYV9faW1nIiBzdHlsZT0id2lkdGg6IDY2cHg7IGhlaWdodDogNDhweDsgbGVmdDogLTFw -eDsgdG9wOiAwcHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFt -ZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24i -IHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3RvcmFt -YV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzkz -LmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDogNzhweDsgaGVpZ2h0OiA0 -OHB4OyBsZWZ0OiAtN3B4OyB0b3A6IDBweDsiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9y -YW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAi -IHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFf -X3RodW1iIGZvdG9yYW1hX19sb2FkZWQgZm90b3JhbWFfX2xvYWRlZC0taW1nIj48aW1nIHNyYz0i -LzIwMjAvMTJfMTAvOTQuanBnIiBjbGFzcz0iZm90b3JhbWFfX2ltZyIgc3R5bGU9IndpZHRoOiA2 -OHB4OyBoZWlnaHQ6IDQ4cHg7IGxlZnQ6IC0ycHg7IHRvcDogMHB4OyI+PC9kaXY+PC9kaXY+PGRp -diBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1i -IiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNs -YXNzPSJmb3RvcmFtYV9fdGh1bWIgZm90b3JhbWFfX2xvYWRlZCBmb3RvcmFtYV9fbG9hZGVkLS1p -bWciPjxpbWcgc3JjPSIvMjAyMC8xMl8xMC85NS5qcGciIGNsYXNzPSJmb3RvcmFtYV9faW1nIiBz -dHlsZT0id2lkdGg6IDY4cHg7IGhlaWdodDogNDhweDsgbGVmdDogLTJweDsgdG9wOiAwcHg7Ij48 -L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2 -X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDog -NjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9y -YW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzk2LmpwZyIgY2xhc3M9ImZv -dG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDogNjRweDsgaGVpZ2h0OiA5MXB4OyBsZWZ0OiAwcHg7 -IHRvcDogLTIycHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFt -ZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24i -IHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3RvcmFt -YV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEwLzk3 -LmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDogNjRweDsgaGVpZ2h0OiA5 -MXB4OyBsZWZ0OiAwcHg7IHRvcDogLTIycHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3Rv -cmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIw -IiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1h -X190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9 -Ii8yMDIwLzEyXzEwLzk4LmpwZyIgY2xhc3M9ImZvdG9yYW1hX19pbWciIHN0eWxlPSJ3aWR0aDog -NjhweDsgaGVpZ2h0OiA0OHB4OyBsZWZ0OiAtMnB4OyB0b3A6IDBweDsiPjwvZGl2PjwvZGl2Pjxk -aXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVt -YiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBj -bGFzcz0iZm90b3JhbWFfX3RodW1iIGZvdG9yYW1hX19sb2FkZWQgZm90b3JhbWFfX2xvYWRlZC0t -aW1nIj48aW1nIHNyYz0iLzIwMjAvMTJfMTAvOTkuanBnIiBjbGFzcz0iZm90b3JhbWFfX2ltZyIg -c3R5bGU9IndpZHRoOiA3MHB4OyBoZWlnaHQ6IDQ4cHg7IGxlZnQ6IC0zcHg7IHRvcDogMHB4OyI+ -PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25h -dl9fZnJhbWUtLXRodW1iIGZvdG9yYW1hX19hY3RpdmUiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0 -b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiBmb3Rv -cmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBzcmM9Ii8yMDIwLzEyXzEw -LzEwMC5qcGciIGNsYXNzPSJmb3RvcmFtYV9faW1nIiBzdHlsZT0id2lkdGg6IDY4cHg7IGhlaWdo -dDogNDhweDsgbGVmdDogLTJweDsgdG9wOiAwcHg7Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJm -b3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4 -PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9y -YW1hX190aHVtYiBmb3RvcmFtYV9fbG9hZGVkIGZvdG9yYW1hX19sb2FkZWQtLWltZyI+PGltZyBz -cmM9Ii8yMDIwLzEyXzEwLzEwMS5qcGciIGNsYXNzPSJmb3RvcmFtYV9faW1nIiBzdHlsZT0id2lk -dGg6IDY4cHg7IGhlaWdodDogNDhweDsgbGVmdDogLTJweDsgdG9wOiAwcHg7Ij48L2Rpdj48L2Rp -dj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0t -dGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxk -aXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3Jh -bWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIg -cm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9f -dGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9y -YW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9 -IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48 -ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1 -bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYg -Y2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFf -X25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9s -ZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1 -bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1h -X19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9Indp -ZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2 -IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIi -IHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xh -c3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25h -dl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0i -YnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIi -PjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19u -YXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRo -OiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNs -YXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRh -YmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9 -ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9f -ZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0 -dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwv -ZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZf -X2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2 -NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNz -PSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmlu -ZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZv -dG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJh -bWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9u -IiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2 -PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2Zy -YW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4 -OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJm -b3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4 -PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9y -YW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUg -Zm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBz -dHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2Pjwv -ZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1l -LS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+ -PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3Rv -cmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIw -IiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1h -X190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90 -b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHls -ZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2 -PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10 -aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRp -diBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFt -YV9fbmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiBy -b2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190 -aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3Jh -bWFfX25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0i -d2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2Pjxk -aXYgY2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVt -YiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBj -bGFzcz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9f -bmF2X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xl -PSJidXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVt -YiI+PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFf -X25hdl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lk -dGg6IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYg -Y2xhc3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIg -dGFiaW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFz -cz0iZm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2 -X19mcmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJi -dXR0b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+ -PC9kaXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25h -dl9fZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6 -IDY0cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xh -c3M9ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFi -aW5kZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0i -Zm90b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19m -cmFtZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0 -b24iIHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9k -aXY+PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9f -ZnJhbWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0 -cHg7Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9 -ImZvdG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5k -ZXg9IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90 -b3JhbWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFt -ZSBmb3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24i -IHN0eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+ -PC9kaXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJh -bWUtLXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7 -Ij48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZv -dG9yYW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9 -IjAiIHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3Jh -bWFfX3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBm -b3RvcmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0 -eWxlPSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9k -aXY+PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUt -LXRodW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48 -ZGl2IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9y -YW1hX19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAi -IHJvbGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFf -X3RodW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3Rv -cmFtYV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxl -PSJ3aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+ -PGRpdiBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRo -dW1iIiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2 -IGNsYXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1h -X19uYXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJv -bGU9ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3Ro -dW1iIj48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFt -YV9fbmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3 -aWR0aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRp -diBjbGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1i -IiB0YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNs -YXNzPSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19u -YXZfX2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9 -ImJ1dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1i -Ij48L2Rpdj48L2Rpdj48ZGl2IGNsYXNzPSJmb3RvcmFtYV9fbmF2X19mcmFtZSBmb3RvcmFtYV9f -bmF2X19mcmFtZS0tdGh1bWIiIHRhYmluZGV4PSIwIiByb2xlPSJidXR0b24iIHN0eWxlPSJ3aWR0 -aDogNjRweDsiPjxkaXYgY2xhc3M9ImZvdG9yYW1hX190aHVtYiI+PC9kaXY+PC9kaXY+PGRpdiBj -bGFzcz0iZm90b3JhbWFfX25hdl9fZnJhbWUgZm90b3JhbWFfX25hdl9fZnJhbWUtLXRodW1iIiB0 -YWJpbmRleD0iMCIgcm9sZT0iYnV0dG9uIiBzdHlsZT0id2lkdGg6IDY0cHg7Ij48ZGl2IGNsYXNz -PSJmb3RvcmFtYV9fdGh1bWIiPjwvZGl2PjwvZGl2PjxkaXYgY2xhc3M9ImZvdG9yYW1hX19uYXZf -X2ZyYW1lIGZvdG9yYW1hX19uYXZfX2ZyYW1lLS10aHVtYiIgdGFiaW5kZXg9IjAiIHJvbGU9ImJ1 -dHRvbiIgc3R5bGU9IndpZHRoOiA2NHB4OyI+PGRpdiBjbGFzcz0iZm90b3JhbWFfX3RodW1iIj48 -L2Rpdj48L2Rpdj48L2Rpdj48L2Rpdj48L2Rpdj48L2Rpdj48L2Rpdj48IS0tIDwvYmM+IC0tPiAK -PC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2Pgo8IS0tIDwvYmxvY2s5ODA1PiAt -LT4KCjwhLS0gPGJsb2NrNT4gLS0+Cgo8IS0tIDwvYmxvY2s1PiAtLT4KCjwhLS0gPGJsb2NrODE1 -NT4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3giPjxkaXYgY2xhc3M9InNpZGV0aXRsZSI+PHNwYW4+ -PCEtLSA8YnQ+IC0tPtCt0LvQtdC60YLRgNC+0L3QvdGL0LUg0YPRgdC70YPQs9C4PCEtLSA8L2J0 -PiAtLT48L3NwYW4+PC9kaXY+CjxkaXYgY2xhc3M9ImlubmVyIj4KPCEtLSA8YmM+IC0tPjxoMiBz -dHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iL2luZGV4LzAtMyI+0K3Qm9CV0JrQ -otCg0J7QndCd0J7QlSDQntCR0KDQkNCp0JXQndCY0JU8L2E+PC9oMj48IS0tIDwvYmM+IC0tPiAK -PC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2Pgo8IS0tIDwvYmxvY2s4MTU1PiAt -LT4KCjwhLS0gPGJsb2NrMz4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3giPjxkaXYgY2xhc3M9InNp -ZGV0aXRsZSI+PHNwYW4+PCEtLSA8YnQ+IC0tPtCg0LDQt9C00LXQuzwhLS0gPC9idD4gLS0+PC9z -cGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJpbm5lciI+CjwhLS0gPGJjPiAtLT48cCBzdHlsZT0idGV4 -dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iaHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL2RyaXZl -L2ZvbGRlcnMvMWRxR2pfOXB4MjE2a3ZJTFJ4cFZFcno1R0JxN1M4OHQ4P3VzcD1zaGFyZV9saW5r -Ij48c3BhbiBzdHlsZT0iZm9udC1zaXplOjE2cHg7Ij7QotC10YXQvdC40LrQsCDQsdC10LfQvtC/ -0LDRgdC90L7RgdGC0Lg8L3NwYW4+PC9hPjwvcD4KPHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRl -cjsiPjxhIGhyZWY9Imh0dHA6Ly9wb2xpdGVobmlrdW0tZW5nLnJ1L2luZGV4L2FudGl0ZXJyb3Iv -MC01MTAiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTZweDsiPtCQ0L3RgtC40YLQtdGA0YDQvtGA -0LjRgdGC0LjRh9C10YHQutCw0Y8g0LTQtdGP0YLQtdC70YzQvdC+0YHRgtGMPC9zcGFuPjwvYT48 -L3A+CjxwIHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij48YSBocmVmPSJodHRwOi8vcG9saXRl -aG5pa3VtLWVuZy5ydS9pbmRleC8wLTQxNSI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxNnB4OyI+ -0JDQvdGC0LjQutC+0YDRgNGD0L/RhtC40L7QvdC90LDRjyDQtNC10Y/RgtC10LvRjNC90L7RgdGC -0Yw8L3NwYW4+PC9hPjwvcD4KPHAgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9 -Imh0dHBzOi8vZGlnaXRhbC1saWtiZXouZGF0YWxlc3Nvbi5ydS8iPjxzcGFuIHN0eWxlPSJmb250 -LXNpemU6MTZweDsiPtCf0YDQvtC10LrRgiDRhtC40YTRgNC+0LLQvtC50LvQuNC60LHQtdC3LtGA -0YQ8L3NwYW4+PC9hPjwvcD48IS0tIDwvYmM+IC0tPiAKPC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+ -PC9kaXY+CjwvZGl2Pgo8IS0tIDwvYmxvY2szPiAtLT4KCjwhLS0gPGJsb2NrOD4gLS0+Cgo8IS0t -IDwvYmxvY2s4PiAtLT4KCjwhLS0gPGJsb2NrNjg5ND4gLS0+CjxkaXYgY2xhc3M9InNpZGVib3gi -PjxkaXYgY2xhc3M9InNpZGV0aXRsZSI+PHNwYW4+PCEtLSA8YnQ+IC0tPtCd0YPQttC90LAg0L/Q -vtC80L7RidGMPzwhLS0gPC9idD4gLS0+PC9zcGFuPjwvZGl2Pgo8ZGl2IGNsYXNzPSJpbm5lciI+ -CjwhLS0gPGJjPiAtLT48cCBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+CjxhIGNsYXNzPSJ1 -bGlnaHRib3giIGhyZWY9Ii8yMDIzLzA1XzE2LzIyLmpwZyIgc3R5bGU9ImZvbnQtc2l6ZTogMTJw -dDsiIHRhcmdldD0iX2JsYW5rIiB0aXRsZT0i0J3QsNC20LzQuNGC0LUsINC00LvRjyDQv9GA0L7R -gdC80L7RgtGA0LAg0LIg0L/QvtC70L3QvtC8INGA0LDQt9C80LXRgNC1Li4uIj48aW1nIGFsaWdu -PSIiIGFsdD0iIiBzcmM9Ii8yMDIzLzA1XzE2LzIyLmpwZyIgc3R5bGU9InBhZGRpbmc6MTtib3Jk -ZXI6IDBweDsgaGVpZ2h0OiAzMDBweDsiPjwvYT48L3A+PCEtLSA8L2JjPiAtLT4gCjwvZGl2Pgo8 -ZGl2IGNsYXNzPSJjbHIiPjwvZGl2Pgo8L2Rpdj4KPCEtLSA8L2Jsb2NrNjg5ND4gLS0+Cgo8IS0t -IDxibG9jazE0NjE+IC0tPgo8ZGl2IGNsYXNzPSJzaWRlYm94Ij48ZGl2IGNsYXNzPSJzaWRldGl0 -bGUiPjxzcGFuPjwhLS0gPGJ0PiAtLT7Qn9GA0Y/QvNCw0Y8g0YLRgNCw0L3RgdC70Y/RhtC40Y88 -IS0tIDwvYnQ+IC0tPjwvc3Bhbj48L2Rpdj4KPGRpdiBjbGFzcz0iaW5uZXIiPgo8IS0tIDxiYz4g -LS0+PGgyIHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij7Qn9GA0Y/QvNCw0Y8g0YLRgNCw0L3R -gdC70Y/RhtC40Y8g0JfQvdCw0L3QuNC1LtCi0JI8L2gyPgo8aDIgc3R5bGU9InRleHQtYWxpZ246 -IGNlbnRlcjsiPjxpbWcgc3JjPSIvMjAyMy8xMV8xMC96bmFuaWUucG5nIiBhbHQ9IiIgd2lkdGg9 -IjQ1JSI+PGltZyBzcmM9Ii8yMDIzLzExXzEwL3puYW5pZTIucG5nIiBhbHQ9IiIgd2lkdGg9IjQ1 -JSI+PC9oMj4KPGRpdj7QodGB0YvQu9C60LAg0L3QsCDQv9GA0L7RgdCy0LXRgtC40YLQtdC70YzR -gdC60LjQuSDQvNCw0YDQsNGE0L7QvSDQl9C90LDQvdC40LUu0J/QtdGA0LLRi9C1OjwvZGl2Pgo8 -ZGl2Pgo8ZGl2PjxhIGhyZWY9Imh0dHBzOi8vem5hbmllcnVzc2lhLnJ1L3MvdHYiIHRhcmdldD0i -X2JsYW5rIj5odHRwczovL3puYW5pZXJ1c3NpYS5ydS9zL3R2PC9hPjwvZGl2Pgo8L2Rpdj48IS0t -IDwvYmM+IC0tPiAKPC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2Pgo8IS0tIDwv -YmxvY2sxNDYxPiAtLT4KCjwhLS0gPGJsb2NrMTA+IC0tPgoKPGRpdiBjbGFzcz0ic2lkZWJveCI+ -PGRpdiBjbGFzcz0ic2lkZXRpdGxlIj48c3Bhbj48IS0tIDxidD4gLS0+0JDRgNGF0LjQsiDQvdC+ -0LLQvtGB0YLQtdC5PCEtLSA8L2J0PiAtLT48L3NwYW4+PC9kaXY+CjxkaXYgY2xhc3M9ImlubmVy -Ij4KPCEtLSA8YmM+IC0tPjxzZWxlY3QgY2xhc3M9ImFyY2hNZW51IiBuYW1lPSJhcmNobWVudSIg -b25jaGFuZ2U9InRvcC5sb2NhdGlvbi5ocmVmPScvbmV3cy8nK3RoaXMub3B0aW9uc1t0aGlzLnNl -bGVjdGVkSW5kZXhdLnZhbHVlOyI+PG9wdGlvbiB2YWx1ZT0iIj4tINCS0YvQsdC10YDQuNGC0LUg -0LzQtdGB0Y/RhiAtPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxMy0wMSI+MjAxMyDQr9C90LLQ -sNGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDEzLTAyIj4yMDEzINCk0LXQstGA0LDQu9GM -PC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxMy0wMyI+MjAxMyDQnNCw0YDRgjwvb3B0aW9uPjxv -cHRpb24gdmFsdWU9IjIwMTMtMDQiPjIwMTMg0JDQv9GA0LXQu9GMPC9vcHRpb24+PG9wdGlvbiB2 -YWx1ZT0iMjAxMy0wNSI+MjAxMyDQnNCw0Lk8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDEzLTA2 -Ij4yMDEzINCY0Y7QvdGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxMy0wNyI+MjAxMyDQmNGO -0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTMtMDgiPjIwMTMg0JDQstCz0YPRgdGCPC9v -cHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxMy0wOSI+MjAxMyDQodC10L3RgtGP0LHRgNGMPC9vcHRp -b24+PG9wdGlvbiB2YWx1ZT0iMjAxMy0xMCI+MjAxMyDQntC60YLRj9Cx0YDRjDwvb3B0aW9uPjxv -cHRpb24gdmFsdWU9IjIwMTMtMTEiPjIwMTMg0J3QvtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2 -YWx1ZT0iMjAxMy0xMiI+MjAxMyDQlNC10LrQsNCx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9 -IjIwMTQtMDEiPjIwMTQg0K/QvdCy0LDRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNC0w -MiI+MjAxNCDQpNC10LLRgNCw0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTQtMDMiPjIw -MTQg0JzQsNGA0YI8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE0LTA0Ij4yMDE0INCQ0L/RgNC1 -0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTQtMDUiPjIwMTQg0JzQsNC5PC9vcHRpb24+ -PG9wdGlvbiB2YWx1ZT0iMjAxNC0wNiI+MjAxNCDQmNGO0L3RjDwvb3B0aW9uPjxvcHRpb24gdmFs -dWU9IjIwMTQtMDciPjIwMTQg0JjRjtC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE0LTA4 -Ij4yMDE0INCQ0LLQs9GD0YHRgjwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTQtMDkiPjIwMTQg -0KHQtdC90YLRj9Cx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTQtMTAiPjIwMTQg0J7Q -utGC0Y/QsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE0LTExIj4yMDE0INCd0L7Rj9Cx -0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTQtMTIiPjIwMTQg0JTQtdC60LDQsdGA0Yw8 -L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE1LTAxIj4yMDE1INCv0L3QstCw0YDRjDwvb3B0aW9u -PjxvcHRpb24gdmFsdWU9IjIwMTUtMDIiPjIwMTUg0KTQtdCy0YDQsNC70Yw8L29wdGlvbj48b3B0 -aW9uIHZhbHVlPSIyMDE1LTAzIj4yMDE1INCc0LDRgNGCPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0i -MjAxNS0wNCI+MjAxNSDQkNC/0YDQtdC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE1LTA1 -Ij4yMDE1INCc0LDQuTwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTUtMDYiPjIwMTUg0JjRjtC9 -0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE1LTA4Ij4yMDE1INCQ0LLQs9GD0YHRgjwvb3B0 -aW9uPjxvcHRpb24gdmFsdWU9IjIwMTUtMDkiPjIwMTUg0KHQtdC90YLRj9Cx0YDRjDwvb3B0aW9u -PjxvcHRpb24gdmFsdWU9IjIwMTUtMTAiPjIwMTUg0J7QutGC0Y/QsdGA0Yw8L29wdGlvbj48b3B0 -aW9uIHZhbHVlPSIyMDE1LTExIj4yMDE1INCd0L7Rj9Cx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFs -dWU9IjIwMTUtMTIiPjIwMTUg0JTQtdC60LDQsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIy -MDE2LTAxIj4yMDE2INCv0L3QstCw0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTYtMDIi -PjIwMTYg0KTQtdCy0YDQsNC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE2LTAzIj4yMDE2 -INCc0LDRgNGCPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNi0wNCI+MjAxNiDQkNC/0YDQtdC7 -0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE2LTA1Ij4yMDE2INCc0LDQuTwvb3B0aW9uPjxv -cHRpb24gdmFsdWU9IjIwMTYtMDYiPjIwMTYg0JjRjtC90Yw8L29wdGlvbj48b3B0aW9uIHZhbHVl -PSIyMDE2LTA3Ij4yMDE2INCY0Y7Qu9GMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNi0wOCI+ -MjAxNiDQkNCy0LPRg9GB0YI8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE2LTA5Ij4yMDE2INCh -0LXQvdGC0Y/QsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE2LTEwIj4yMDE2INCe0LrR -gtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNi0xMSI+MjAxNiDQndC+0Y/QsdGA -0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE2LTEyIj4yMDE2INCU0LXQutCw0LHRgNGMPC9v -cHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNy0wMSI+MjAxNyDQr9C90LLQsNGA0Yw8L29wdGlvbj48 -b3B0aW9uIHZhbHVlPSIyMDE3LTAyIj4yMDE3INCk0LXQstGA0LDQu9GMPC9vcHRpb24+PG9wdGlv -biB2YWx1ZT0iMjAxNy0wMyI+MjAxNyDQnNCw0YDRgjwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIw -MTctMDQiPjIwMTcg0JDQv9GA0LXQu9GMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNy0wNSI+ -MjAxNyDQnNCw0Lk8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE3LTA2Ij4yMDE3INCY0Y7QvdGM -PC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNy0wNyI+MjAxNyDQmNGO0LvRjDwvb3B0aW9uPjxv -cHRpb24gdmFsdWU9IjIwMTctMDgiPjIwMTcg0JDQstCz0YPRgdGCPC9vcHRpb24+PG9wdGlvbiB2 -YWx1ZT0iMjAxNy0wOSI+MjAxNyDQodC10L3RgtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1 -ZT0iMjAxNy0xMCI+MjAxNyDQntC60YLRj9Cx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIw -MTctMTEiPjIwMTcg0J3QvtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxNy0xMiI+ -MjAxNyDQlNC10LrQsNCx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTgtMDEiPjIwMTgg -0K/QvdCy0LDRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxOC0wMiI+MjAxOCDQpNC10LLR -gNCw0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTgtMDMiPjIwMTgg0JzQsNGA0YI8L29w -dGlvbj48b3B0aW9uIHZhbHVlPSIyMDE4LTA0Ij4yMDE4INCQ0L/RgNC10LvRjDwvb3B0aW9uPjxv -cHRpb24gdmFsdWU9IjIwMTgtMDUiPjIwMTgg0JzQsNC5PC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0i -MjAxOC0wNiI+MjAxOCDQmNGO0L3RjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTgtMDciPjIw -MTgg0JjRjtC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE4LTA4Ij4yMDE4INCQ0LLQs9GD -0YHRgjwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTgtMDkiPjIwMTgg0KHQtdC90YLRj9Cx0YDR -jDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTgtMTAiPjIwMTgg0J7QutGC0Y/QsdGA0Yw8L29w -dGlvbj48b3B0aW9uIHZhbHVlPSIyMDE4LTExIj4yMDE4INCd0L7Rj9Cx0YDRjDwvb3B0aW9uPjxv -cHRpb24gdmFsdWU9IjIwMTgtMTIiPjIwMTgg0JTQtdC60LDQsdGA0Yw8L29wdGlvbj48b3B0aW9u -IHZhbHVlPSIyMDE5LTAxIj4yMDE5INCv0L3QstCw0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9 -IjIwMTktMDIiPjIwMTkg0KTQtdCy0YDQsNC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE5 -LTAzIj4yMDE5INCc0LDRgNGCPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxOS0wNCI+MjAxOSDQ -kNC/0YDQtdC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE5LTA1Ij4yMDE5INCc0LDQuTwv -b3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMTktMDYiPjIwMTkg0JjRjtC90Yw8L29wdGlvbj48b3B0 -aW9uIHZhbHVlPSIyMDE5LTA3Ij4yMDE5INCY0Y7Qu9GMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0i -MjAxOS0wOCI+MjAxOSDQkNCy0LPRg9GB0YI8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE5LTA5 -Ij4yMDE5INCh0LXQvdGC0Y/QsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE5LTEwIj4y -MDE5INCe0LrRgtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAxOS0xMSI+MjAxOSDQ -ndC+0Y/QsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDE5LTEyIj4yMDE5INCU0LXQutCw -0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMC0wMSI+MjAyMCDQr9C90LLQsNGA0Yw8 -L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIwLTAyIj4yMDIwINCk0LXQstGA0LDQu9GMPC9vcHRp -b24+PG9wdGlvbiB2YWx1ZT0iMjAyMC0wMyI+MjAyMCDQnNCw0YDRgjwvb3B0aW9uPjxvcHRpb24g -dmFsdWU9IjIwMjAtMDQiPjIwMjAg0JDQv9GA0LXQu9GMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0i -MjAyMC0wNSI+MjAyMCDQnNCw0Lk8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIwLTA2Ij4yMDIw -INCY0Y7QvdGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMC0wNyI+MjAyMCDQmNGO0LvRjDwv -b3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjAtMDgiPjIwMjAg0JDQstCz0YPRgdGCPC9vcHRpb24+ -PG9wdGlvbiB2YWx1ZT0iMjAyMC0wOSI+MjAyMCDQodC10L3RgtGP0LHRgNGMPC9vcHRpb24+PG9w -dGlvbiB2YWx1ZT0iMjAyMC0xMCI+MjAyMCDQntC60YLRj9Cx0YDRjDwvb3B0aW9uPjxvcHRpb24g -dmFsdWU9IjIwMjAtMTEiPjIwMjAg0J3QvtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0i -MjAyMC0xMiI+MjAyMCDQlNC10LrQsNCx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjEt -MDEiPjIwMjEg0K/QvdCy0LDRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMS0wMiI+MjAy -MSDQpNC10LLRgNCw0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjEtMDMiPjIwMjEg0JzQ -sNGA0YI8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIxLTA0Ij4yMDIxINCQ0L/RgNC10LvRjDwv -b3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjEtMDUiPjIwMjEg0JzQsNC5PC9vcHRpb24+PG9wdGlv -biB2YWx1ZT0iMjAyMS0wNiI+MjAyMSDQmNGO0L3RjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIw -MjEtMDciPjIwMjEg0JjRjtC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIxLTA4Ij4yMDIx -INCQ0LLQs9GD0YHRgjwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjEtMDkiPjIwMjEg0KHQtdC9 -0YLRj9Cx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjEtMTAiPjIwMjEg0J7QutGC0Y/Q -sdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIxLTExIj4yMDIxINCd0L7Rj9Cx0YDRjDwv -b3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjEtMTIiPjIwMjEg0JTQtdC60LDQsdGA0Yw8L29wdGlv -bj48b3B0aW9uIHZhbHVlPSIyMDIyLTAxIj4yMDIyINCv0L3QstCw0YDRjDwvb3B0aW9uPjxvcHRp -b24gdmFsdWU9IjIwMjItMDIiPjIwMjIg0KTQtdCy0YDQsNC70Yw8L29wdGlvbj48b3B0aW9uIHZh -bHVlPSIyMDIyLTAzIj4yMDIyINCc0LDRgNGCPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMi0w -NCI+MjAyMiDQkNC/0YDQtdC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIyLTA1Ij4yMDIy -INCc0LDQuTwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjItMDYiPjIwMjIg0JjRjtC90Yw8L29w -dGlvbj48b3B0aW9uIHZhbHVlPSIyMDIyLTA3Ij4yMDIyINCY0Y7Qu9GMPC9vcHRpb24+PG9wdGlv -biB2YWx1ZT0iMjAyMi0wOCI+MjAyMiDQkNCy0LPRg9GB0YI8L29wdGlvbj48b3B0aW9uIHZhbHVl -PSIyMDIyLTA5Ij4yMDIyINCh0LXQvdGC0Y/QsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIy -MDIyLTEwIj4yMDIyINCe0LrRgtGP0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMi0x -MSI+MjAyMiDQndC+0Y/QsdGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIyLTEyIj4yMDIy -INCU0LXQutCw0LHRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMy0wMSI+MjAyMyDQr9C9 -0LLQsNGA0Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIzLTAyIj4yMDIzINCk0LXQstGA0LDQ -u9GMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMy0wMyI+MjAyMyDQnNCw0YDRgjwvb3B0aW9u -PjxvcHRpb24gdmFsdWU9IjIwMjMtMDQiPjIwMjMg0JDQv9GA0LXQu9GMPC9vcHRpb24+PG9wdGlv -biB2YWx1ZT0iMjAyMy0wNSI+MjAyMyDQnNCw0Lk8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDIz -LTA2Ij4yMDIzINCY0Y7QvdGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMy0wNyI+MjAyMyDQ -mNGO0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjMtMDgiPjIwMjMg0JDQstCz0YPRgdGC -PC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMy0wOSI+MjAyMyDQodC10L3RgtGP0LHRgNGMPC9v -cHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAyMy0xMCI+MjAyMyDQntC60YLRj9Cx0YDRjDwvb3B0aW9u -PjxvcHRpb24gdmFsdWU9IjIwMjMtMTEiPjIwMjMg0J3QvtGP0LHRgNGMPC9vcHRpb24+PG9wdGlv -biB2YWx1ZT0iMjAyMy0xMiI+MjAyMyDQlNC10LrQsNCx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFs -dWU9IjIwMjQtMDEiPjIwMjQg0K/QvdCy0LDRgNGMPC9vcHRpb24+PG9wdGlvbiB2YWx1ZT0iMjAy -NC0wMiI+MjAyNCDQpNC10LLRgNCw0LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjQtMDMi -PjIwMjQg0JzQsNGA0YI8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDI0LTA0Ij4yMDI0INCQ0L/R -gNC10LvRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjQtMDUiPjIwMjQg0JzQsNC5PC9vcHRp -b24+PG9wdGlvbiB2YWx1ZT0iMjAyNC0wNiI+MjAyNCDQmNGO0L3RjDwvb3B0aW9uPjxvcHRpb24g -dmFsdWU9IjIwMjQtMDciPjIwMjQg0JjRjtC70Yw8L29wdGlvbj48b3B0aW9uIHZhbHVlPSIyMDI0 -LTA4Ij4yMDI0INCQ0LLQs9GD0YHRgjwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjQtMDkiPjIw -MjQg0KHQtdC90YLRj9Cx0YDRjDwvb3B0aW9uPjxvcHRpb24gdmFsdWU9IjIwMjQtMTAiPjIwMjQg -0J7QutGC0Y/QsdGA0Yw8L29wdGlvbj48L3NlbGVjdD48IS0tIDwvYmM+IC0tPiAKPC9kaXY+Cjxk -aXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2PgoKPCEtLSA8L2Jsb2NrMTA+IC0tPgoKPCEtLSA8 -YmxvY2s2PiAtLT4KCjxkaXYgY2xhc3M9InNpZGVib3giPjxkaXYgY2xhc3M9InNpZGV0aXRsZSI+ -PHNwYW4+PCEtLSA8YnQ+IC0tPjwhLS08czUxNTg+LS0+0JLRhdC+0LQg0L3QsCDRgdCw0LnRgjwh -LS08L3M+LS0+PCEtLSA8L2J0PiAtLT48L3NwYW4+PC9kaXY+CjxkaXYgY2xhc3M9ImlubmVyIj4K -PCEtLSA8YmM+IC0tPjxkaXYgaWQ9InVpZExvZ0Zvcm0iIGFsaWduPSJjZW50ZXIiPjxhIGhyZWY9 -ImphdmFzY3JpcHQ6OyIgb25jbGljaz0id2luZG93Lm9wZW4oJ2h0dHBzOi8vbG9naW4udWlkLm1l -Lz9zaXRlPTBwdC1lbmdlbHMmYW1wO3JlZj0nK2VzY2FwZShsb2NhdGlvbi5wcm90b2NvbCArICcv -LycgKyAoJ3BvbGl0ZWhuaWt1bS1lbmcucnUnIHx8IGxvY2F0aW9uLmhvc3RuYW1lKSArIGxvY2F0 -aW9uLnBhdGhuYW1lICsgKChsb2NhdGlvbi5oYXNoID8gKCBsb2NhdGlvbi5zZWFyY2ggPyBsb2Nh -dGlvbi5zZWFyY2ggKyAnJmFtcDsnIDogJz8nICkgKyAncm5kPScgKyBEYXRlLm5vdygpICsgbG9j -YXRpb24uaGFzaCA6ICggbG9jYXRpb24uc2VhcmNoIHx8ICcnICkpKSksJ3VpZExvZ2luV25kJywn -d2lkdGg9NTgwLGhlaWdodD00NTAscmVzaXphYmxlPXllcyx0aXRsZWJhcj15ZXMnKTtyZXR1cm4g -ZmFsc2U7IiBjbGFzcz0ibG9naW4td2l0aCB1aWQiIHRpdGxlPSLQktC+0LnRgtC4INGH0LXRgNC1 -0LcgdUlEIiByZWw9Im5vZm9sbG93Ij48aT48L2k+PC9hPjxhIGhyZWY9ImphdmFzY3JpcHQ6OyIg -b25jbGljaz0icmV0dXJuIHVTb2NpYWxMb2dpbigndmtvbnRha3RlJyk7IiBkYXRhLXNvY2lhbD0i -dmtvbnRha3RlIiBjbGFzcz0ibG9naW4td2l0aCB2a29udGFrdGUiIHRpdGxlPSLQktC+0LnRgtC4 -INGH0LXRgNC10Lcg0JLQmtC+0L3RgtCw0LrRgtC1IiByZWw9Im5vZm9sbG93Ij48aT48L2k+PC9h -PjxhIGhyZWY9ImphdmFzY3JpcHQ6OyIgb25jbGljaz0icmV0dXJuIHVTb2NpYWxMb2dpbignZmFj -ZWJvb2snKTsiIGRhdGEtc29jaWFsPSJmYWNlYm9vayIgY2xhc3M9ImxvZ2luLXdpdGggZmFjZWJv -b2siIHRpdGxlPSLQktC+0LnRgtC4INGH0LXRgNC10LcgRmFjZWJvb2siIHJlbD0ibm9mb2xsb3ci -PjxpPjwvaT48L2E+PGEgaHJlZj0iamF2YXNjcmlwdDo7IiBvbmNsaWNrPSJyZXR1cm4gdVNvY2lh -bExvZ2luKCd5YW5kZXgnKTsiIGRhdGEtc29jaWFsPSJ5YW5kZXgiIGNsYXNzPSJsb2dpbi13aXRo -IHlhbmRleCIgdGl0bGU9ItCS0L7QudGC0Lgg0YfQtdGA0LXQtyDQr9C90LTQtdC60YEiIHJlbD0i -bm9mb2xsb3ciPjxpPjwvaT48L2E+PGEgaHJlZj0iamF2YXNjcmlwdDo7IiBvbmNsaWNrPSJyZXR1 -cm4gdVNvY2lhbExvZ2luKCdnb29nbGUnKTsiIGRhdGEtc29jaWFsPSJnb29nbGUiIGNsYXNzPSJs -b2dpbi13aXRoIGdvb2dsZSIgdGl0bGU9ItCS0L7QudGC0Lgg0YfQtdGA0LXQtyBHb29nbGUiIHJl -bD0ibm9mb2xsb3ciPjxpPjwvaT48L2E+PGEgaHJlZj0iamF2YXNjcmlwdDo7IiBvbmNsaWNrPSJy -ZXR1cm4gdVNvY2lhbExvZ2luKCdvaycpOyIgZGF0YS1zb2NpYWw9Im9rIiBjbGFzcz0ibG9naW4t -d2l0aCBvayIgdGl0bGU9ItCS0L7QudGC0Lgg0YfQtdGA0LXQtyDQntC00L3QvtC60LvQsNGB0YHQ -vdC40LrQuCIgcmVsPSJub2ZvbGxvdyI+PGk+PC9pPjwvYT48L2Rpdj48IS0tIDwvYmM+IC0tPiAK -PC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2PgoKPCEtLSA8L2Jsb2NrNj4gLS0+ -Cgo8IS0tIDxibG9jazE0PiAtLT4KPGRpdiBjbGFzcz0ic2lkZWJveCI+PGRpdiBjbGFzcz0ic2lk -ZXRpdGxlIj48c3Bhbj48IS0tIDxidD4gLS0+PCEtLTxzNTE5NT4tLT7QodGC0LDRgtC40YHRgtC4 -0LrQsDwhLS08L3M+LS0+PCEtLSA8L2J0PiAtLT48L3NwYW4+PC9kaXY+CjxkaXYgY2xhc3M9Imlu -bmVyIj4KPGRpdiBhbGlnbj0iY2VudGVyIj48IS0tIDxiYz4gLS0+PCEtLSBZYW5kZXguTWV0cmlr -YSBpbmZvcm1lciAtLT4KPGEgaHJlZj0iaHR0cHM6Ly9tZXRyaWthLnlhbmRleC5ydS9zdGF0Lz9p -ZD0xOTgyMTI0MSZhbXA7ZnJvbT1pbmZvcm1lciIgdGFyZ2V0PSJfYmxhbmsiIHJlbD0ibm9mb2xs -b3ciPjxpbWcgc3JjPSJodHRwczovL2luZm9ybWVyLnlhbmRleC5ydS9pbmZvcm1lci8xOTgyMTI0 -MS8zXzFfRkZGRkZGRkZfRUZFRkVGRkZfMF9wYWdldmlld3MiIHN0eWxlPSJ3aWR0aDo4OHB4OyBo -ZWlnaHQ6MzFweDsgYm9yZGVyOjA7IiBhbHQ9ItCv0L3QtNC10LrRgS7QnNC10YLRgNC40LrQsCIg -dGl0bGU9ItCv0L3QtNC10LrRgS7QnNC10YLRgNC40LrQsDog0LTQsNC90L3Ri9C1INC30LAg0YHQ -tdCz0L7QtNC90Y8gKNC/0YDQvtGB0LzQvtGC0YDRiywg0LLQuNC30LjRgtGLINC4INGD0L3QuNC6 -0LDQu9GM0L3Ri9C1INC/0L7RgdC10YLQuNGC0LXQu9C4KSIgY2xhc3M9InltLWFkdmFuY2VkLWlu -Zm9ybWVyIiBkYXRhLWNpZD0iMTk4MjEyNDEiIGRhdGEtbGFuZz0icnUiPjwvYT4KPCEtLSAvWWFu -ZGV4Lk1ldHJpa2EgaW5mb3JtZXIgLS0+Cgo8IS0tIFlhbmRleC5NZXRyaWthIGNvdW50ZXIgLS0+ -CjxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij4KIChmdW5jdGlvbihtLGUsdCxyLGksayxh -KXttW2ldPW1baV18fGZ1bmN0aW9uKCl7KG1baV0uYT1tW2ldLmF8fFtdKS5wdXNoKGFyZ3VtZW50 -cyl9OwogbVtpXS5sPTEqbmV3IERhdGUoKTtrPWUuY3JlYXRlRWxlbWVudCh0KSxhPWUuZ2V0RWxl -bWVudHNCeVRhZ05hbWUodClbMF0say5hc3luYz0xLGsuc3JjPXIsYS5wYXJlbnROb2RlLmluc2Vy -dEJlZm9yZShrLGEpfSkKICh3aW5kb3csIGRvY3VtZW50LCAic2NyaXB0IiwgImh0dHBzOi8vbWMu -eWFuZGV4LnJ1L21ldHJpa2EvdGFnLmpzIiwgInltIik7CgogeW0oMTk4MjEyNDEsICJpbml0Iiwg -ewogY2xpY2ttYXA6dHJ1ZSwKIHRyYWNrTGlua3M6dHJ1ZSwKIGFjY3VyYXRlVHJhY2tCb3VuY2U6 -dHJ1ZSwKIHdlYnZpc29yOnRydWUKIH0pOwo8L3NjcmlwdD4KPG5vc2NyaXB0PjxkaXY+PGltZyBz -cmM9Imh0dHBzOi8vbWMueWFuZGV4LnJ1L3dhdGNoLzE5ODIxMjQxIiBzdHlsZT0icG9zaXRpb246 -YWJzb2x1dGU7IGxlZnQ6LTk5OTlweDsiIGFsdD0iIiAvPjwvZGl2Pjwvbm9zY3JpcHQ+CjwhLS0g -L1lhbmRleC5NZXRyaWthIGNvdW50ZXIgLS0+Cgo8YSByZWw9Im5vZm9sbG93IiBocmVmPSIvcGFu -ZWwvP2E9dXN0YXQmYW1wO3U9cHQtZW5nZWxzJmFtcDtkPTAmYW1wO2lsPXJ1IiB0YXJnZXQ9Il9i -bGFuayIgdGl0bGU9InVDb3ogQ291bnRlciI+Cgk8aW1nIGFsdD0iIiBzcmM9Ii9zdGF0LzE3Mjgy -MzE4NDEiIGhlaWdodD0iMzEiIHdpZHRoPSI4OCI+PC9hPjxocj48ZGl2IGNsYXNzPSJ0T25saW5l -IiBpZD0ib25sMSI+0J7QvdC70LDQudC9INCy0YHQtdCz0L46IDxiPjQ8L2I+PC9kaXY+IDxkaXYg -Y2xhc3M9ImdPbmxpbmUiIGlkPSJvbmwyIj7Qk9C+0YHRgtC10Lk6IDxiPjI8L2I+PC9kaXY+IDxk -aXYgY2xhc3M9InVPbmxpbmUiIGlkPSJvbmwzIj7Qn9C+0LvRjNC30L7QstCw0YLQtdC70LXQuTog -PGI+MjwvYj48L2Rpdj48YSBjbGFzcz0iZ3JvdXBVc2VyIiBocmVmPSJqYXZhc2NyaXB0OjsiIHJl -bD0ibm9mb2xsb3ciIG9uY2xpY2s9IndpbmRvdy5vcGVuKCcvaW5kZXgvOC04MjQnLCAndXA4MjQn -LCAnc2Nyb2xsYmFycz0xLHRvcD0wLGxlZnQ9MCxyZXNpemFibGU9MSx3aWR0aD03MDAsaGVpZ2h0 -PTM3NScpOyByZXR1cm4gZmFsc2U7Ij5tcmhhbnN0aWQ8L2E+LCA8YSBjbGFzcz0iZ3JvdXBVc2Vy -IiBocmVmPSJqYXZhc2NyaXB0OjsiIHJlbD0ibm9mb2xsb3ciIG9uY2xpY2s9IndpbmRvdy5vcGVu -KCcvaW5kZXgvOC04NzUnLCAndXA4NzUnLCAnc2Nyb2xsYmFycz0xLHRvcD0wLGxlZnQ9MCxyZXNp -emFibGU9MSx3aWR0aD03MDAsaGVpZ2h0PTM3NScpOyByZXR1cm4gZmFsc2U7Ij5uYXRhZmlsaXB1 -c2hlbmtvMjAxNTwvYT48IS0tIDwvYmM+IC0tPjwvZGl2PiAKPC9kaXY+CjxkaXYgY2xhc3M9ImNs -ciI+PC9kaXY+CjwvZGl2Pgo8IS0tIDwvYmxvY2sxND4gLS0+Cgo8IS0tIDxibG9jazEzPiAtLT4K -PGRpdiBjbGFzcz0ic2lkZWJveCI+PGRpdiBjbGFzcz0ic2lkZXRpdGxlIj48c3Bhbj48IS0tIDxi -dD4gLS0+PCEtLTxzNTIwND4tLT7QlNGA0YPQt9GM0Y8g0YHQsNC50YLQsDwhLS08L3M+LS0+PCEt -LSA8L2J0PiAtLT48L3NwYW4+PC9kaXY+CjxkaXYgY2xhc3M9ImlubmVyIj4KPCEtLSA8YmM+IC0t -PjxkaXYgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Imh0dHBzOi8vd3d3Lm1p -bm9icm5hdWtpLmdvdi5ydS8iIHRhcmdldD0iX2JsYW5rIj48aW1nIHNyYz0iLzIwMTQvbW9yZi5q -cGciIHRpdGxlPSLQnNC40L3QuNGB0YLQtdGA0YHRgtCy0LAg0L3QsNGD0LrQuCDQuCDQstGL0YHR -iNC10LPQviDQvtCx0YDQsNC30L7QstCw0L3QuNGPINCg0L7RgdGB0LjQudGB0LrQvtC5INCk0LXQ -tNC10YDQsNGG0LjQuCIgYWx0PSLQnNC40L3QuNGB0YLQtdGA0YHRgtCy0LAg0L3QsNGD0LrQuCDQ -uCDQstGL0YHRiNC10LPQviDQvtCx0YDQsNC30L7QstCw0L3QuNGPINCg0L7RgdGB0LjQudGB0LrQ -vtC5INCk0LXQtNC10YDQsNGG0LjQuCIgc3R5bGU9IiIgd2lkdGg9IjI1MHB4IiBoZWlnaHQ9Ijg4 -cHgiPjwvYT48L2Rpdj4KPGRpdiBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGJyPjwvZGl2 -Pgo8ZGl2IHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij48YSBocmVmPSJodHRwOi8vZWR1Lmdv -di5ydSIgdGFyZ2V0PSJfYmxhbmsiPjxpbWcgc3JjPSIvMjAxNC9lZHUuanBnIiBhbHQ9IiIgc3R5 -bGU9IiIgd2lkdGg9IjI1MHB4IiBoZWlnaHQ9Ijg1cHgiPjwvYT48YnI+PC9kaXY+CjxkaXYgc3R5 -bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxicj48L2Rpdj4KPGRpdiBzdHlsZT0idGV4dC1hbGln -bjogY2VudGVyOyI+PGEgaHJlZj0iaHR0cHM6Ly9kbmV2bmlrLnJ1LyIgdGFyZ2V0PSJfYmxhbmsi -PjxpbWcgc3JjPSIvMjAxNC9kbmV2bmlrLnJ1LnBuZyIgYWx0PSIiIHN0eWxlPSIiIHdpZHRoPSIy -NTBweCIgaGVpZ2h0PSI2NnB4Ij48L2E+PGJyPjwvZGl2Pgo8ZGl2IHN0eWxlPSJ0ZXh0LWFsaWdu -OiBjZW50ZXI7Ij48YnI+PC9kaXY+Cgo8ZGl2IHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij48 -YSBocmVmPSJodHRwOi8vbWlub2JyLnNhcmF0b3YuZ292LnJ1LyIgdGFyZ2V0PSJfYmxhbmsiPjxp -bWcgc3JjPSIvMjAxNC9tb3MuanBnIiBhbHQ9IiIgc3R5bGU9IiIgd2lkdGg9IjI1MHB4IiBoZWln -aHQ9IjY2cHgiPjwvYT48YnI+PC9kaXY+CjxkaXYgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsi -Pjxicj48L2Rpdj4KPGRpdiBzdHlsZT0idGV4dC1hbGlnbjogY2VudGVyOyI+PGEgaHJlZj0iaHR0 -cDovL3d3dy5lZHUucnUvIiB0YXJnZXQ9Il9ibGFuayI+PGltZyBzcmM9Ii8yMDE0L29icmF6b3Yx -LmpwZyIgYWx0PSIiIHN0eWxlPSIiIHdpZHRoPSIyNTBweCIgaGVpZ2h0PSI2M3B4Ij48L2E+PGJy -PjwvZGl2Pgo8ZGl2IHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij48YnI+PC9kaXY+CjxkaXYg -c3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Imh0dHA6Ly9zZHBvdTY0LmJpdHJp -eDI0LnNpdGUvIiB0YXJnZXQ9Il9ibGFuayI+PGltZyBzcmM9Ii8yMDE0L3Nkb3ljcG8uanBnIiBh -bHQ9IiIgc3R5bGU9IiIgd2lkdGg9IjI1MHB4IiBoZWlnaHQ9IjYwcHgiPjwvYT48YnI+PC9kaXY+ -CjxkaXYgc3R5bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxicj48L2Rpdj4KCjxkaXYgc3R5bGU9 -InRleHQtYWxpZ246IGNlbnRlcjsiPjxhIGhyZWY9Imh0dHA6Ly9wcmF2by5nb3YucnUvIiB0YXJn -ZXQ9Il9ibGFuayI+PGltZyBzcmM9Ii8yMDE0L3BvcnRhbF9nb3YuanBnIiBhbHQ9IiIgc3R5bGU9 -IiIgd2lkdGg9IjI1MHB4IiBoZWlnaHQ9IjYwcHgiPjwvYT48YnI+PC9kaXY+CjxkaXYgc3R5bGU9 -InRleHQtYWxpZ246IGNlbnRlcjsiPjxicj48L2Rpdj4KPGRpdiBzdHlsZT0idGV4dC1hbGlnbjog -Y2VudGVyOyI+PGEgaHJlZj0iaHR0cHM6Ly91cmFpdC5ydS8iIHRhcmdldD0iX2JsYW5rIj48aW1n -IHNyYz0iaHR0cHM6Ly91cmFpdC5ydS9pbWFnZXMvbG9nb18yMDZ4NTYuc3ZnIiBhbHQ9IiIgc3R5 -bGU9IiIgd2lkdGg9IjI1MHB4IiBoZWlnaHQ9IjYwcHgiPjwvYT48YnI+PC9kaXY+CjxkaXYgc3R5 -bGU9InRleHQtYWxpZ246IGNlbnRlcjsiPjxicj48L2Rpdj4KPGRpdiBzdHlsZT0idGV4dC1hbGln -bjogY2VudGVyOyI+PGEgaHJlZj0iaHR0cDovL3d3dy5hY2FkZW1pYS1tb3Njb3cucnUvIiB0YXJn -ZXQ9Il9ibGFuayI+PGltZyBzcmM9Ii8yMDE4L2FjYWRlbS5wbmciIGFsdD0iIiBzdHlsZT0iIiB3 -aWR0aD0iMjUwcHgiIGhlaWdodD0iNjBweCI+PC9hPjxicj48L2Rpdj48IS0tIDwvYmM+IC0tPiAK -PC9kaXY+CjxkaXYgY2xhc3M9ImNsciI+PC9kaXY+CjwvZGl2Pgo8IS0tIDwvYmxvY2sxMz4gLS0+ -CjwhLS0vVTFDTEVGVEVSMVotLT4KIDwvZGl2PgogPC9hc2lkZT4KIDwhLS0gPC9taWRkbGU+IC0t -PgogPGRpdiBjbGFzcz0iY2xyIj48L2Rpdj4KIAogPC9kaXY+CjwvZGl2Pgo8IS0tVTFCRk9PVEVS -MVotLT48Zm9vdGVyPgogPGRpdiBjbGFzcz0id3JhcHBlciI+CiA8ZGl2IGlkPSJmb290ZXIiPgog -PGRpdiBjbGFzcz0iZm9vdC1yIj4KIDxkaXYgY2xhc3M9InNvYy1ib3giPgogPGEgaHJlZj0iLy92 -ay5jb20vIiBjbGFzcz0ic29jLXZrIiB0YXJnZXQ9Il9ibGFuayI+PC9hPgogPGEgaHJlZj0iaHR0 -cHM6Ly93d3cuZmFjZWJvb2suY29tLyIgY2xhc3M9InNvYy1mYyIgdGFyZ2V0PSJfYmxhbmsiPjwv -YT4KIDxhIGhyZWY9Imh0dHBzOi8vdHdpdHRlci5jb20vIiBjbGFzcz0ic29jLXR3IiB0YXJnZXQ9 -Il9ibGFuayI+PC9hPgogPGEgaHJlZj0iaHR0cDovL3d3dy5vZG5va2xhc3NuaWtpLnJ1LyIgY2xh -c3M9InNvYy1vZCIgdGFyZ2V0PSJfYmxhbmsiPjwvYT4KIDwvZGl2PgogPC9kaXY+CiA8ZGl2IGNs -YXNzPSJmb290LWwiPgogPCEtLSA8Y29weT4gLS0+0JPQkNCf0J7QoyDQodCeIMKr0K3QvdCz0LXQ -u9GM0YHRgdC60LjQuSDQv9C+0LvQuNGC0LXRhdC90LjQutGD0LzCuyDCqSAyMDI0PCEtLSA8L2Nv -cHk+IC0tPi4gPCEtLSBZYW5kZXguTWV0cmlrYSBjb3VudGVyIC0tPjxzY3JpcHQgc3JjPSIvL21j -LnlhbmRleC5ydS9tZXRyaWthL3dhdGNoLmpzIiB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiPjwvc2Ny -aXB0PjxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij50cnkgeyB2YXIgeWFDb3VudGVyMTk4 -MjEyNDEgPSBuZXcgWWEuTWV0cmlrYSh7aWQ6MTk4MjEyNDF9KTt9IGNhdGNoKGUpIHsgfTwvc2Ny -aXB0Pjxub3NjcmlwdD48ZGl2PjxpbWcgc3JjPSIvL21jLnlhbmRleC5ydS93YXRjaC8xOTgyMTI0 -MSIgc3R5bGU9InBvc2l0aW9uOmFic29sdXRlOyBsZWZ0Oi05OTk5cHg7IiBhbHQ9IiIgLz48L2Rp -dj48L25vc2NyaXB0PjwhLS0gL1lhbmRleC5NZXRyaWthIGNvdW50ZXIgLS0+IDxhIGhyZWY9Imh0 -dHA6Ly9wb2xpdGVobmlrdW0tZW5nLnJ1L2luZGV4LzAtMyIgdGl0bGU9ItCe0LHRgNCw0YLQvdCw -0Y8g0YHQstGP0LfRjCI+0J7QsdGA0LDRgtC90LDRjyDRgdCy0Y/Qt9GMPC9hPgogPC9kaXY+CiA8 -ZGl2IGNsYXNzPSJjbHIiPjwvZGl2PgogPC9kaXY+CiA8L2Rpdj4KPC9mb290ZXI+PCEtLS9VMUJG -T09URVIxWi0tPgo8c2NyaXB0IHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSIvLnMvdC8yMDEx -L3RlbXBsYXRlLm1pbi5qcyI+PC9zY3JpcHQ+CjxsaW5rIGhyZWY9Ii8xMi9mb3RvcmFtYS5jc3Mi -IHJlbD0ic3R5bGVzaGVldCI+PHNjcmlwdCBzcmM9Ii8xMi9mb3RvcmFtYS5qcyI+PC9zY3JpcHQ+ -CjxkaXYgc3R5bGU9ImRpc3BsYXk6bm9uZSI+Cgo8L2Rpdj4KCgo8L2JvZHk+PC9odG1sPgo= diff --git a/tsconfig.json b/tsconfig.json index 95f5641..ed7fc07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "builder": "swc", "module": "commonjs", "declaration": true, "removeComments": true,