diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd2ff246..5abdd847 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: with: arguments: test - name: Upload coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./server/build/reports/jacoco/test/jacocoTestReport.xml diff --git a/.gitignore b/.gitignore index a69ccd3c..4bcd381b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,12 @@ out/ ### Data ### data +dcConstants.json ### OpenAPI ### openapitools.json ### App Config ### server/src/main/resources/application.yml + + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..b1402343 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "player_code"] + path = server/src/main/resources/player_code + url = https://github.com/delta/codecharacter-default-codes-2022 diff --git a/.runConfigurations/Format.run.xml b/.runConfigurations/Format.run.xml index 84d7259f..f043e364 100644 --- a/.runConfigurations/Format.run.xml +++ b/.runConfigurations/Format.run.xml @@ -20,4 +20,4 @@ false - \ No newline at end of file + diff --git a/Dockerfile b/Dockerfile index 576c050f..5776b94e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:7.3.3-jdk17 as base +FROM gradle:7.6-jdk17 as base WORKDIR /server COPY build.gradle.kts settings.gradle.kts gradlew ./ COPY library/build.gradle.kts library/settings.gradle.kts ./library/ diff --git a/README.md b/README.md index 5b38d25f..008e2016 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -

CodeCharacter Server 2022

+

CodeCharacter Server 2023

- - + + - - + + - - + +

@@ -20,10 +20,14 @@ ### Setup 1. Clone the repo and open in IntelliJ Ultimate. -2. Press `Ctrl+Alt+Shift+S` and make sure the JDK version is 17. -3. Press `Ctrl+Alt+S` and go to `Build, Execution, Deployment -> Docker` and make sure docker is configured correctly/ -4. Copy `server/src/main/resources/application.example.yml` to `server/src/main/resources/application.yml`. If you want to use docker instead, copy the `server/src/main/resources/application.docker.example.yml` to `server/src/main/resources/application.yml`. -5. The run configurations will be available in the top bar: +2. From the project root directory,run ```./gradlew installGitHooks``` to install git-hooks +3. Press `Ctrl+Alt+Shift+S` and make sure the JDK version is 17. +4. Press `Ctrl+Alt+S` and go to `Build, Execution, Deployment -> Docker` and make sure docker is configured correctly/ +5. Copy `server/src/main/resources/application.example.yml` to `server/src/main/resources/application.yml`. If you want to use docker instead, copy the `server/src/main/resources/application.docker.example.yml` to `server/src/main/resources/application.yml`. +6. Run `git submodule update --init` +7. Run `cp server/src/main/resources/dcConstans.example.json server/src/main/resources/dcConstans.json` +8. The run configurations will be available in the top bar: + ![Run Configurations](https://i.imgur.com/pO2SrPd.png) ### Run Configurations diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3a84268d..cc021261 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -44,6 +44,7 @@ services: networks: - common + networks: common: name: codecharacter_common diff --git a/docs/README.md b/docs/README.md index e67ff4a7..7d84220d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# CodeCharacter Server 2022 +# CodeCharacter Server 2023 ### Code Generation Process diff --git a/docs/spec/CodeCharacter-API.yml b/docs/spec/CodeCharacter-API.yml index 6b6b123b..4f39a800 100644 --- a/docs/spec/CodeCharacter-API.yml +++ b/docs/spec/CodeCharacter-API.yml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: CodeCharacter API - version: 2022.0.1 + version: 2023.0.1 contact: name: CodeCharacter Authors url: 'https://delta.nitt.edu' @@ -145,6 +145,7 @@ paths: Example: value: email: user@example.com + recaptchaCode: example recaptch code /leaderboard: get: summary: Get leaderboard @@ -296,8 +297,14 @@ paths: in: query name: size description: Size of the page + - schema: + $ref: '#/components/schemas/TierType' + in: query + name: tier + description: Leaderboard Tier description: Get leaderboard parameters: [] + /top-matches: get: summary: Get top matches @@ -528,7 +535,10 @@ paths: country: IN college: NITT avatarId: 1 + tutorialLevel: 2 + tier: TIER1 isProfileComplete: true + isTutorialComplete: true '401': description: Unauthorized operationId: getCurrentUser @@ -564,6 +574,7 @@ paths: name: Test User Mofified country: IN college: NIT Trichy + updateTutorialLevel: NEXT tags: - current-user /user/complete-profile: @@ -651,6 +662,12 @@ paths: '401': description: Unauthorized operationId: getLatestCode + parameters: + - schema: + $ref: '#/components/schemas/CodeType' + in: query + name: type + description: code type description: Get latest code post: summary: Update latest code @@ -666,6 +683,7 @@ paths: Example: value: code: '#include' + codeType: NORMAL language: C lock: false responses: @@ -730,6 +748,12 @@ paths: '401': description: Unauthorized operationId: getCodeRevisions + parameters: + - schema: + $ref: '#/components/schemas/CodeType' + in: query + name: type + description: code type description: Get list of all code revision IDs post: summary: Create code revision @@ -760,6 +784,7 @@ paths: Example: value: code: '#include' + codeType: NORMAL message: revision1 language: C tags: @@ -782,11 +807,18 @@ paths: Example: value: map: 0000\n0010\n0100\n1000\n + mapImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII' lastSavedAt: '2019-08-24T14:15:22Z' '401': description: Unauthorized operationId: getLatestMap description: Get latest map + parameters: + - schema: + $ref: '#/components/schemas/GameMapType' + in: query + name: type + description: map type post: summary: Update latest map operationId: updateLatestMap @@ -801,6 +833,8 @@ paths: Example: value: map: 0000\n0010\n0100\n1000\n + mapType: NORMAL + mapImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII' lock: false responses: '204': @@ -852,6 +886,12 @@ paths: '401': description: Unauthorized operationId: getMapRevisions + parameters: + - schema: + $ref: '#/components/schemas/GameMapType' + in: query + name: type + description: map type description: Get list of all map revision IDs post: summary: Create map revision @@ -882,10 +922,41 @@ paths: Example: value: map: 0000\n0010\n0100\n1000\n + mapImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII' + mapType: NORMAL message: revision1 tags: - map parameters: [] + /user/map/revision/{commitId}: + get: + summary: Get the Map and image of the commit ID + tags: + - map + responses: + 200: + description: OK + headers: { } + content: + application/json: + schema: + $ref: '#/components/schemas/MapCommitByCommitIdResponse' + examples: + Example: + value: + mapImage: 'base-64-string' + map: 0000\n0000\0001\n0000 + 401: + description: Unauthorized + operationId: getMapByCommitID + parameters: + - schema: + type: string + format: uuid + example: 123e456-e445b-12d3-a4565-212324 + in: path + required: true + name: commitId /user/matches: get: summary: Get user matches @@ -1064,6 +1135,7 @@ paths: country: IN college: NITT avatarId: 0 + recaptchaCode: 03AD1IbLAGl_UdwYP3-AeibnfJgXy_g3cNr_rhkBBh4zalD9GEXAR2xCcUGi7WlxFgOjYlpbRpZFTJJDVugJF-H4pBl32DU619cYHplp_ReGiOokgvz8DwiRLIZBvg1eu2e77jihWQPndoWU_WOTKrYVq1mzBcdPUfJ3PEMCo-eGvoyRaNvRWE0JYBSBgDfwFBaw8RmxaqiS84or-_G7-TDiifFYpcNFiIolIjGi9DkbMXivkjiIoEomAz6NUHg0alrk0C5_p1maoErBmpwLGwlAgKL_sa-ZAzHb89OprdVI8BXtN0CATBgwYO6u_zqrK5N9wDQyh-OmtFh5RXkEzmkASls33UYcJrtMfeFU-b9N-u-Je6NXVfkX49gAGan3k-GqkgcFKHowc5Cwym9tlGLrfiBtqKLIADw1UX4BCbIx9BbHlesoKEubr7MoVZCDv3VfctSTMXG-oH5IbDRQhez4E6JHR4Uv0lWyHKROv7wdxqXauz5PBlUlE11BdffXU5NEssJkM4Tk3zg5k6ddkju8DU2keqXodnzXVTBIXC6zxriA8IHaS_KFBtazAYZ6oac3-5Y2VMwli3XaADBCCVJzXC0GTa1jeuZQ description: '' security: [] '/users/{userId}/activate': @@ -1128,6 +1200,100 @@ paths: description: Not Found operationId: getRatingHistory description: Get user rating history + + '/dc/get': + get: + summary: Get Daily Challenge for the day + description: Get current user challenge for that day + operationId: getDailyChallenge + tags: + - Daily Challenges + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DailyChallengeGetRequest' + 401: + description: Unauthorized + 403: + description: Forbidden + 404: + description: Not found + '/dc/leaderboard': + get: + summary: Get Daily Challenges Leaderboard + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DailyChallengeLeaderBoardResponse' + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + description: Get Leaderboard for daily challenges + operationId: getDailyChallengeLeaderBoard + tags: + - Daily Challenges + parameters: + - schema: + type: integer + in: query + name: page + description: Index of the page + - schema: + type: integer + in: query + name: size + description: Size of the page + parameters: [] + /dc/submit: + post: + summary: Match Execution for Daily Challenges + description: Match making for Daily Challenges + operationId: createDailyChallengeMatch + responses: + '201': + description: Created + '400': + description: Bad Request + headers: { } + content: + application/json: + schema: + $ref: '#/components/schemas/GenericError' + examples: + Example: + value: + message: Some field missing + '401': + description: Unauthorized + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DailyChallengeMatchRequest' + examples: + CodeExample: + value: + value : "#include" + language: CPP + MapExample: + value: + value: "[[0,0,0]]" + tags: + - Daily Challenges + + components: schemas: PasswordLoginRequest: @@ -1194,6 +1360,9 @@ components: format: email pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}' example: test@test.com + recaptchaCode: + type: string + example: 'example recaptcha-code' required: - email RatingHistory: @@ -1229,6 +1398,8 @@ components: country: type: string example: IN + tier: + $ref: '#/components/schemas/TierType' college: type: string example: Test @@ -1238,6 +1409,7 @@ components: required: - username - name + - tier - country - college - avatarId @@ -1295,6 +1467,8 @@ components: code: type: string example: '#include ' + codeType: + $ref: '#/components/schemas/CodeType' lock: type: boolean default: false @@ -1341,6 +1515,8 @@ components: code: type: string example: '#include ' + codeType: + $ref: '#/components/schemas/CodeType' message: type: string example: message @@ -1358,12 +1534,16 @@ components: map: type: string example: 0000\n0010\n0100\n1000\n + mapImage: + type: string + example: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII' lastSavedAt: type: string format: date-time example: '2021-01-01T00:00:00Z' required: - map + - mapImage - lastSavedAt UpdateLatestMapRequest: title: UpdateLatestMapRequest @@ -1373,11 +1553,17 @@ components: map: type: string example: 0000\n0010\n0100\n1000\n + mapType: + $ref: '#/components/schemas/GameMapType' + mapImage: + type: string + example: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII' lock: type: boolean default: false required: - map + - mapImage GameMapRevision: title: GameMapRevision type: object @@ -1413,12 +1599,32 @@ components: map: type: string example: 0000\n0010\n0100\n1000\n + mapType: + $ref: '#/components/schemas/GameMapType' + mapImage: + type: string + example: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII' message: type: string example: message required: - map + - mapImage - message + MapCommitByCommitIdResponse: + title: MapCommitByCommitIdResponse + type: object + description: Get map image and map by commitId + properties: + mapImage: + type: string + example: 'base-64-string' + map: + type: string + example: 0000\n0000\n0001\n0000 + required: + - mapImage + - map ActivateUserRequest: title: ActivateUserRequest type: object @@ -1513,9 +1719,17 @@ components: avatarId: type: integer example: 1 + tutorialLevel: + type: integer + example: 1 + tier: + $ref: '#/components/schemas/TierType' isProfileComplete: type: boolean default: false + isTutorialComplete: + type: boolean + default: false required: - id - username @@ -1524,7 +1738,9 @@ components: - country - college - avatarId + - tutorialLevel - isProfileComplete + - isTutorialComplete UpdateCurrentUserProfile: title: UpdateCurrentUserProfile type: object @@ -1546,6 +1762,8 @@ components: type: integer example: 1 nullable: true + updateTutorialLevel: + $ref: '#/components/schemas/TutorialUpdateType' CompleteProfileRequest: title: CompleteProfileRequest type: object @@ -1609,6 +1827,9 @@ components: avatarId: type: integer example: 1 + recaptchaCode: + type: string + example: 03AD1IbLAGl_UdwYP3-AeibnfJgXy_g3cNr_rhkBBh4zalD9GEXAR2xCcUGi7WlxFgOjYlpbRpZFTJJDVugJF-H4pBl32DU619cYHplp_ReGiOokgvz8DwiRLIZBvg1eu2e77jihWQPndoWU_WOTKrYVq1mzBcdPUfJ3PEMCo-eGvoyRaNvRWE0JYBSBgDfwFBaw8RmxaqiS84or-_G7-TDiifFYpcNFiIolIjGi9DkbMXivkjiIoEomAz6NUHg0alrk0C5_p1maoErBmpwLGwlAgKL_sa-ZAzHb89OprdVI8BXtN0CATBgwYO6u_zqrK5N9wDQyh-OmtFh5RXkEzmkASls33UYcJrtMfeFU-b9N-u-Je6NXVfkX49gAGan3k-GqkgcFKHowc5Cwym9tlGLrfiBtqKLIADw1UX4BCbIx9BbHlesoKEubr7MoVZCDv3VfctSTMXG-oH5IbDRQhez4E6JHR4Uv0lWyHKROv7wdxqXauz5PBlUlE11BdffXU5NEssJkM4Tk3zg5k6ddkju8DU2keqXodnzXVTBIXC6zxriA8IHaS_KFBtazAYZ6oac3-5Y2VMwli3XaADBCCVJzXC0GTa1jeuZQ required: - username - name @@ -1618,6 +1839,7 @@ components: - country - college - avatarId + - recaptchaCode Match: description: Match model type: object @@ -1650,7 +1872,6 @@ components: - matchVerdict - createdAt - user1 - - user2 CreateMatchRequest: title: CreateMatchRequest type: object @@ -1718,6 +1939,72 @@ components: - user - stats description: Leaderboard entry model + + DailyChallengeGetRequest: + title: Get daily challenge + description: Get current-user daily challenge + type: object + properties: + challName: + type: string + example: Daily Challenge 1 + description: + type: string + example: Daily Challenge description + chall: + $ref: '#/components/schemas/DailyChallengeObject' + challType: + $ref: '#/components/schemas/ChallengeType' + completionStatus: + type: boolean + example: true + required: + - challName + - chall + - challType + DailyChallengeObject: + title: Daily Challenge Object + description: The object describing the challenge for the day + type: object + properties: + cpp: + type: string + java: + type: string + python: + type: string + image: + type: string + DailyChallengeLeaderBoardResponse: + title: DailyChallengeLeaderboardResponse + description: Response model for daily challenge leaderboard + type: object + properties: + userName: + type: string + example: TestUser + score: + type: number + example: 1500.00 + avatarId: + type: integer + example: 1 + required: + - userName + - score + - avatarId + DailyChallengeMatchRequest: + title: DailyChallengeMatchRequest + description: Request Model for the daily challenge + type: object + properties: + value: + type: string + example: "#include" + language: + $ref: '#/components/schemas/Language' + required: + - value GenericError: title: GenericError type: object @@ -1744,6 +2031,7 @@ components: - SELF - MANUAL - AUTO + - DAILYCHALLENGE description: Match Mode Verdict: type: string @@ -1752,6 +2040,8 @@ components: - PLAYER1 - PLAYER2 - TIE + - SUCCESS + - FAILURE description: Match/Game verdict GameStatus: title: GameStatus @@ -1771,6 +2061,43 @@ components: - AUTHENTICATED - PROFILE_INCOMPLETE - ACTIVATION_PENDING + ChallengeType: + title: DailyChallengeTypes + type: string + enum: + - CODE + - MAP + TutorialUpdateType: + title: TutorialUpdateType + type: string + enum: + - NEXT + - PREVIOUS + - SKIP + - RESET + CodeType: + title: CodeType + type: string + enum: + - NORMAL + - DAILY_CHALLENGE + default: NORMAL + GameMapType: + title: GameMapType + type: string + enum: + - NORMAL + - DAILY_CHALLENGE + default: NORMAL + TierType: + title: TierType + type: string + enum: + - TIER_PRACTICE + - TIER1 + - TIER2 + - TIER3 + - TIER4 securitySchemes: oauth2-google: type: oauth2 diff --git a/docs/spec/generator-config.yml b/docs/spec/generator-config.yml index 07567eaa..3f2241b4 100644 --- a/docs/spec/generator-config.yml +++ b/docs/spec/generator-config.yml @@ -1,6 +1,6 @@ apiPackage: delta.codecharacter.core artifactId: codecharacter-core -artifactVersion: 2022.0.1 +artifactVersion: 2023.0.1 basePackage: delta.codecharacter delegatePattern: false enumPropertyNaming: UPPERCASE @@ -21,3 +21,6 @@ swaggerAnnotations: true title: CodeCharacter Core API useBeanValidation: true useTags: true +useJakartaEe: true +springVersion: 3.0.0 +useSpringBoot3: true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897..070cb702 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library/.openapi-generator/FILES b/library/.openapi-generator/FILES index b5c7363e..e90390f3 100644 --- a/library/.openapi-generator/FILES +++ b/library/.openapi-generator/FILES @@ -1,6 +1,8 @@ +src/main/kotlin/delta/codecharacter/SpringDocConfiguration.kt src/main/kotlin/delta/codecharacter/core/AuthApi.kt src/main/kotlin/delta/codecharacter/core/CodeApi.kt src/main/kotlin/delta/codecharacter/core/CurrentUserApi.kt +src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt src/main/kotlin/delta/codecharacter/core/GameApi.kt src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt src/main/kotlin/delta/codecharacter/core/MapApi.kt @@ -9,21 +11,29 @@ src/main/kotlin/delta/codecharacter/core/NotificationApi.kt src/main/kotlin/delta/codecharacter/core/UserApi.kt src/main/kotlin/delta/codecharacter/dtos/ActivateUserRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/AuthStatusResponseDto.kt +src/main/kotlin/delta/codecharacter/dtos/ChallengeTypeDto.kt src/main/kotlin/delta/codecharacter/dtos/CodeDto.kt src/main/kotlin/delta/codecharacter/dtos/CodeRevisionDto.kt +src/main/kotlin/delta/codecharacter/dtos/CodeTypeDto.kt src/main/kotlin/delta/codecharacter/dtos/CompleteProfileRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/CreateCodeRevisionRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/CreateMapRevisionRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/CreateMatchRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/CurrentUserProfileDto.kt +src/main/kotlin/delta/codecharacter/dtos/DailyChallengeGetRequestDto.kt +src/main/kotlin/delta/codecharacter/dtos/DailyChallengeLeaderBoardResponseDto.kt +src/main/kotlin/delta/codecharacter/dtos/DailyChallengeMatchRequestDto.kt +src/main/kotlin/delta/codecharacter/dtos/DailyChallengeObjectDto.kt src/main/kotlin/delta/codecharacter/dtos/ForgotPasswordRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/GameDto.kt src/main/kotlin/delta/codecharacter/dtos/GameMapDto.kt src/main/kotlin/delta/codecharacter/dtos/GameMapRevisionDto.kt +src/main/kotlin/delta/codecharacter/dtos/GameMapTypeDto.kt src/main/kotlin/delta/codecharacter/dtos/GameStatusDto.kt src/main/kotlin/delta/codecharacter/dtos/GenericErrorDto.kt src/main/kotlin/delta/codecharacter/dtos/LanguageDto.kt src/main/kotlin/delta/codecharacter/dtos/LeaderboardEntryDto.kt +src/main/kotlin/delta/codecharacter/dtos/MapCommitByCommitIdResponseDto.kt src/main/kotlin/delta/codecharacter/dtos/MatchDto.kt src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt src/main/kotlin/delta/codecharacter/dtos/NotificationDto.kt @@ -33,6 +43,8 @@ src/main/kotlin/delta/codecharacter/dtos/PublicUserDto.kt src/main/kotlin/delta/codecharacter/dtos/RatingHistoryDto.kt src/main/kotlin/delta/codecharacter/dtos/RegisterUserRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/ResetPasswordRequestDto.kt +src/main/kotlin/delta/codecharacter/dtos/TierTypeDto.kt +src/main/kotlin/delta/codecharacter/dtos/TutorialUpdateTypeDto.kt src/main/kotlin/delta/codecharacter/dtos/UpdateCurrentUserProfileDto.kt src/main/kotlin/delta/codecharacter/dtos/UpdateLatestCodeRequestDto.kt src/main/kotlin/delta/codecharacter/dtos/UpdateLatestMapRequestDto.kt diff --git a/library/.openapi-generator/VERSION b/library/.openapi-generator/VERSION index 7d3cdbf0..e7e42a4b 100644 --- a/library/.openapi-generator/VERSION +++ b/library/.openapi-generator/VERSION @@ -1 +1 @@ -5.3.1 \ No newline at end of file +6.3.0 \ No newline at end of file diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 7fa8e7c8..7f0af371 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile group = "delta.codecharacter" -version = "2022.0.1" +version = "2023.0.1" repositories { mavenCentral() @@ -15,7 +15,7 @@ tasks.withType { } plugins { - id("org.springframework.boot") version "2.6.2" apply false + id("org.springframework.boot") version "3.0.0" apply false id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") kotlin("plugin.jpa") @@ -31,17 +31,19 @@ dependencyManagement { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.0") implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.0") - implementation("org.springframework.boot:spring-boot-starter-validation:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-web:2.6.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") - implementation("io.swagger:swagger-annotations:1.6.4") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.1") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") + implementation("org.springframework.boot:spring-boot-starter-validation:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-web:3.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("jakarta.annotation:jakarta.annotation-api:2.1.1") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.14.0") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") + implementation("io.swagger.core.v3:swagger-annotations:2.2.7") + implementation("io.swagger.core.v3:swagger-core:2.2.7") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.6.0") - testImplementation("org.springframework.boot:spring-boot-starter-test:2.6.3") { + testImplementation("org.springframework.boot:spring-boot-starter-test:3.0.0") { exclude(module = "junit") } } diff --git a/library/src/main/kotlin/delta/codecharacter/SpringDocConfiguration.kt b/library/src/main/kotlin/delta/codecharacter/SpringDocConfiguration.kt new file mode 100644 index 00000000..1ff48624 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/SpringDocConfiguration.kt @@ -0,0 +1,50 @@ +package delta.codecharacter + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.License +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.security.SecurityScheme + +@jakarta.annotation.Generated(value = ["org.openapitools.codegen.languages.KotlinSpringServerCodegen"]) +@Configuration +class SpringDocConfiguration { + + @Bean + fun apiInfo(): OpenAPI { + return OpenAPI() + .info( + Info() + .title("CodeCharacter API") + .description("Specification of the CodeCharacter API") + .contact( + Contact() + .name("CodeCharacter Authors") + .url("https://delta.nitt.edu") + .email("delta@nitt.edu") + ) + .license( + License() + .name("MIT") + ) + .version("2023.0.1") + ) + .components( + Components() + .addSecuritySchemes("http-bearer", SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + ) + .addSecuritySchemes("oauth2-github", SecurityScheme() + .type(SecurityScheme.Type.OAUTH2) + ) + .addSecuritySchemes("oauth2-google", SecurityScheme() + .type(SecurityScheme.Type.OAUTH2) + ) + ) + } +} diff --git a/library/src/main/kotlin/delta/codecharacter/core/AuthApi.kt b/library/src/main/kotlin/delta/codecharacter/core/AuthApi.kt index a9b8e424..3ef5cdd0 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/AuthApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/AuthApi.kt @@ -1,8 +1,8 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.AuthStatusResponseDto @@ -11,138 +11,105 @@ import delta.codecharacter.dtos.GenericErrorDto import delta.codecharacter.dtos.PasswordLoginRequestDto import delta.codecharacter.dtos.PasswordLoginResponseDto import delta.codecharacter.dtos.ResetPasswordRequestDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Auth", description = "The Auth API") @RequestMapping("\${api.base-path:}") interface AuthApi { - @ApiOperation( - value = "Forgot password", - nickname = "forgotPassword", - notes = "Request password reset email to be sent when user forgot their password" - ) - @ApiResponses( - value = [ApiResponse( - code = 202, - message = "Accepted" - ), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Forgot password", + operationId = "forgotPassword", + description = "Request password reset email to be sent when user forgot their password", + responses = [ + ApiResponse(responseCode = "202", description = "Accepted"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/auth/forgot-password"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/auth/forgot-password"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun forgotPassword( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody forgotPasswordRequestDto: ForgotPasswordRequestDto - ): ResponseEntity { + fun forgotPassword(@Parameter(description = "", required = true) @Valid @RequestBody forgotPasswordRequestDto: ForgotPasswordRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get authentication status", - nickname = "getAuthStatus", - notes = "Get authentication status: fully authenticated, activation pending and incomplete profile", - response = AuthStatusResponseDto::class, - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = AuthStatusResponseDto::class - )] + @Operation( + summary = "Get authentication status", + operationId = "getAuthStatus", + description = "Get authentication status: fully authenticated, activation pending and incomplete profile", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = AuthStatusResponseDto::class))]) + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/auth/status"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/auth/status"], + produces = ["application/json"] ) fun getAuthStatus(): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Password Login", - nickname = "passwordLogin", - notes = "Login with email and password and get bearer token for authentication", - response = PasswordLoginResponseDto::class - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = PasswordLoginResponseDto::class - ), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse( - code = 401, - message = "Unauthorized", - response = GenericErrorDto::class - )] + @Operation( + summary = "Password Login", + operationId = "passwordLogin", + description = "Login with email and password and get bearer token for authentication", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = PasswordLoginResponseDto::class))]), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]) + ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/auth/login/password"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/auth/login/password"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun passwordLogin( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody passwordLoginRequestDto: PasswordLoginRequestDto - ): ResponseEntity { + fun passwordLogin(@Parameter(description = "", required = true) @Valid @RequestBody passwordLoginRequestDto: PasswordLoginRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Reset password", - nickname = "resetPassword", - notes = "Reset password using the token from password reset email" - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Reset password", + operationId = "resetPassword", + description = "Reset password using the token from password reset email", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/auth/reset-password"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/auth/reset-password"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun resetPassword( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody resetPasswordRequestDto: ResetPasswordRequestDto - ): ResponseEntity { + fun resetPassword(@Parameter(description = "", required = true) @Valid @RequestBody resetPasswordRequestDto: ResetPasswordRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/CodeApi.kt b/library/src/main/kotlin/delta/codecharacter/core/CodeApi.kt index 6c7acc7a..429d227b 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/CodeApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/CodeApi.kt @@ -1,135 +1,117 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.CodeDto import delta.codecharacter.dtos.CodeRevisionDto +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.CreateCodeRevisionRequestDto import delta.codecharacter.dtos.GenericErrorDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Code", description = "The Code API") @RequestMapping("\${api.base-path:}") interface CodeApi { - @ApiOperation( - value = "Create code revision", - nickname = "createCodeRevision", - notes = "Create code revision", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Create code revision", + operationId = "createCodeRevision", + description = "Create code revision", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/code/revisions"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/code/revisions"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun createCodeRevision( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody createCodeRevisionRequestDto: CreateCodeRevisionRequestDto - ): ResponseEntity { + fun createCodeRevision(@Parameter(description = "", required = true) @Valid @RequestBody createCodeRevisionRequestDto: CreateCodeRevisionRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get code revisions", - nickname = "getCodeRevisions", - notes = "Get list of all code revision IDs", - response = CodeRevisionDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = CodeRevisionDto::class, - responseContainer = "List" - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get code revisions", + operationId = "getCodeRevisions", + description = "Get list of all code revision IDs", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = CodeRevisionDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user/code/revisions"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user/code/revisions"], + produces = ["application/json"] ) - fun getCodeRevisions(): ResponseEntity> { + fun getCodeRevisions(@Parameter(description = "code type", schema = Schema(allowableValues = ["NORMAL", "DAILY_CHALLENGE"], defaultValue = "NORMAL")) @Valid @RequestParam(value = "type", required = false, defaultValue = "NORMAL") type: CodeTypeDto): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get latest code", - nickname = "getLatestCode", - notes = "Get latest code", - response = CodeDto::class, - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = CodeDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get latest code", + operationId = "getLatestCode", + description = "Get latest code", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = CodeDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user/code/latest"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user/code/latest"], + produces = ["application/json"] ) - fun getLatestCode(): ResponseEntity { + fun getLatestCode(@Parameter(description = "code type", schema = Schema(allowableValues = ["NORMAL", "DAILY_CHALLENGE"], defaultValue = "NORMAL")) @Valid @RequestParam(value = "type", required = false, defaultValue = "NORMAL") type: CodeTypeDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Update latest code", - nickname = "updateLatestCode", - notes = "Update latest code", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Update latest code", + operationId = "updateLatestCode", + description = "Update latest code", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/code/latest"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/code/latest"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun updateLatestCode( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody updateLatestCodeRequestDto: UpdateLatestCodeRequestDto - ): ResponseEntity { + fun updateLatestCode(@Parameter(description = "", required = true) @Valid @RequestBody updateLatestCodeRequestDto: UpdateLatestCodeRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/CurrentUserApi.kt b/library/src/main/kotlin/delta/codecharacter/core/CurrentUserApi.kt index 8757659f..46baa8e0 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/CurrentUserApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/CurrentUserApi.kt @@ -1,8 +1,8 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.CompleteProfileRequestDto @@ -10,130 +10,107 @@ import delta.codecharacter.dtos.CurrentUserProfileDto import delta.codecharacter.dtos.GenericErrorDto import delta.codecharacter.dtos.UpdateCurrentUserProfileDto import delta.codecharacter.dtos.UpdatePasswordRequestDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "CurrentUser", description = "The CurrentUser API") @RequestMapping("\${api.base-path:}") interface CurrentUserApi { - @ApiOperation( - value = "Complete user profile", - nickname = "completeUserProfile", - notes = "Complete the user profile for users who registered using OAuth", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK" - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Complete user profile", + operationId = "completeUserProfile", + description = "Complete the user profile for users who registered using OAuth", + responses = [ + ApiResponse(responseCode = "200", description = "OK"), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/complete-profile"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/complete-profile"], + consumes = ["application/json"] ) - fun completeUserProfile( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody completeProfileRequestDto: CompleteProfileRequestDto - ): ResponseEntity { + fun completeUserProfile(@Parameter(description = "", required = true) @Valid @RequestBody completeProfileRequestDto: CompleteProfileRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get current user profile", - nickname = "getCurrentUser", - notes = "Get current user profile", - response = CurrentUserProfileDto::class, - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = CurrentUserProfileDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get current user profile", + operationId = "getCurrentUser", + description = "Get current user profile", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = CurrentUserProfileDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user"], + produces = ["application/json"] ) fun getCurrentUser(): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Update current user", - nickname = "updateCurrentUser", - notes = "Update current user", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - )] + @Operation( + summary = "Update current user", + operationId = "updateCurrentUser", + description = "Update current user", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]) + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.PATCH], - value = ["/user"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.PATCH], + value = ["/user"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun updateCurrentUser( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody updateCurrentUserProfileDto: UpdateCurrentUserProfileDto - ): ResponseEntity { + fun updateCurrentUser(@Parameter(description = "", required = true) @Valid @RequestBody updateCurrentUserProfileDto: UpdateCurrentUserProfileDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Update password", - nickname = "updatePassword", - notes = "Update password", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse( - code = 401, - message = "Unauthorized" - ), ApiResponse(code = 403, message = "Forbidden")] + @Operation( + summary = "Update password", + operationId = "updatePassword", + description = "Update password", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized"), + ApiResponse(responseCode = "403", description = "Forbidden") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/password"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/password"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun updatePassword( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody updatePasswordRequestDto: UpdatePasswordRequestDto - ): ResponseEntity { + fun updatePassword(@Parameter(description = "", required = true) @Valid @RequestBody updatePasswordRequestDto: UpdatePasswordRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt b/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt new file mode 100644 index 00000000..2950dd4e --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt @@ -0,0 +1,98 @@ +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). + * https://openapi-generator.tech + * Do not edit the class manually. +*/ +package delta.codecharacter.core + +import delta.codecharacter.dtos.DailyChallengeGetRequestDto +import delta.codecharacter.dtos.DailyChallengeLeaderBoardResponseDto +import delta.codecharacter.dtos.DailyChallengeMatchRequestDto +import delta.codecharacter.dtos.GenericErrorDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map + +@Validated +@RequestMapping("\${api.base-path:}") +interface DailyChallengesApi { + + @Operation( + summary = "Match Execution for Daily Challenges", + operationId = "createDailyChallengeMatch", + description = "Match making for Daily Challenges", + responses = [ + ApiResponse(responseCode = "201", description = "Created"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/dc/submit"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun createDailyChallengeMatch(@Parameter(description = "", required = true) @Valid @RequestBody dailyChallengeMatchRequestDto: DailyChallengeMatchRequestDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Get Daily Challenge for the day", + operationId = "getDailyChallenge", + description = "Get current user challenge for that day", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = DailyChallengeGetRequestDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized"), + ApiResponse(responseCode = "403", description = "Forbidden"), + ApiResponse(responseCode = "404", description = "Not found") + ], + security = [ SecurityRequirement(name = "http-bearer") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/dc/get"], + produces = ["application/json"] + ) + fun getDailyChallenge(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Get Daily Challenges Leaderboard", + operationId = "getDailyChallengeLeaderBoard", + description = "Get Leaderboard for daily challenges", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = DailyChallengeLeaderBoardResponseDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized"), + ApiResponse(responseCode = "403", description = "Forbidden"), + ApiResponse(responseCode = "404", description = "Not Found") + ], + security = [ SecurityRequirement(name = "http-bearer") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/dc/leaderboard"], + produces = ["application/json"] + ) + fun getDailyChallengeLeaderBoard(@Parameter(description = "Index of the page") @Valid @RequestParam(value = "page", required = false) page: kotlin.Int?,@Parameter(description = "Size of the page") @Valid @RequestParam(value = "size", required = false) size: kotlin.Int?): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/library/src/main/kotlin/delta/codecharacter/core/GameApi.kt b/library/src/main/kotlin/delta/codecharacter/core/GameApi.kt index 05fce661..9da0379b 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/GameApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/GameApi.kt @@ -1,53 +1,49 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Game", description = "The Game API") @RequestMapping("\${api.base-path:}") interface GameApi { - @ApiOperation( - value = "Get game logs by game ID", - nickname = "getGameLogsByGameId", - notes = "Get game logs by game ID", - response = String::class, - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = String::class - )] + @Operation( + summary = "Get game logs by game ID", + operationId = "getGameLogsByGameId", + description = "Get game logs by game ID", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = kotlin.String::class))]) + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/games/{gameId}/logs"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/games/{gameId}/logs"], + produces = ["application/json"] ) - fun getGameLogsByGameId( - @ApiParam( - value = "UUID of the game", - required = true - ) @PathVariable("gameId") gameId: java.util.UUID - ): ResponseEntity { + fun getGameLogsByGameId(@Parameter(description = "UUID of the game", required = true) @PathVariable("gameId") gameId: java.util.UUID): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt b/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt index 0245f8e2..ea89bd31 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt @@ -1,61 +1,52 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.LeaderboardEntryDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import delta.codecharacter.dtos.TierTypeDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import org.springframework.web.bind.annotation.RequestParam -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Leaderboard", description = "The Leaderboard API") @RequestMapping("\${api.base-path:}") interface LeaderboardApi { - @ApiOperation( - value = "Get leaderboard", - nickname = "getLeaderboard", - notes = "Get leaderboard", - response = LeaderboardEntryDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = LeaderboardEntryDto::class, - responseContainer = "List" - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get leaderboard", + operationId = "getLeaderboard", + description = "Get leaderboard", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = LeaderboardEntryDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/leaderboard"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/leaderboard"], + produces = ["application/json"] ) - fun getLeaderboard( - @ApiParam(value = "Index of the page") @Valid @RequestParam( - value = "page", - required = false - ) page: Int?, - @ApiParam(value = "Size of the page") @Valid @RequestParam( - value = "size", - required = false - ) size: Int? - ): ResponseEntity> { + fun getLeaderboard(@Parameter(description = "Index of the page") @Valid @RequestParam(value = "page", required = false) page: kotlin.Int?,@Parameter(description = "Size of the page") @Valid @RequestParam(value = "size", required = false) size: kotlin.Int?,@Parameter(description = "Leaderboard Tier", schema = Schema(allowableValues = ["TIER_PRACTICE", "TIER1", "TIER2", "TIER3", "TIER4"])) @Valid @RequestParam(value = "tier", required = false) tier: TierTypeDto?): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/MapApi.kt b/library/src/main/kotlin/delta/codecharacter/core/MapApi.kt index dd93fe45..065f5332 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/MapApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/MapApi.kt @@ -1,135 +1,137 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.CreateMapRevisionRequestDto import delta.codecharacter.dtos.GameMapDto import delta.codecharacter.dtos.GameMapRevisionDto +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.GenericErrorDto +import delta.codecharacter.dtos.MapCommitByCommitIdResponseDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Map", description = "The Map API") @RequestMapping("\${api.base-path:}") interface MapApi { - @ApiOperation( - value = "Create map revision", - nickname = "createMapRevision", - notes = "Create map revision", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Create map revision", + operationId = "createMapRevision", + description = "Create map revision", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/map/revisions"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/map/revisions"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun createMapRevision( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody createMapRevisionRequestDto: CreateMapRevisionRequestDto - ): ResponseEntity { + fun createMapRevision(@Parameter(description = "", required = true) @Valid @RequestBody createMapRevisionRequestDto: CreateMapRevisionRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get latest map", - nickname = "getLatestMap", - notes = "Get latest map", - response = GameMapDto::class, - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = GameMapDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get latest map", + operationId = "getLatestMap", + description = "Get latest map", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = GameMapDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user/map/latest"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user/map/latest"], + produces = ["application/json"] ) - fun getLatestMap(): ResponseEntity { + fun getLatestMap(@Parameter(description = "map type", schema = Schema(allowableValues = ["NORMAL", "DAILY_CHALLENGE"], defaultValue = "NORMAL")) @Valid @RequestParam(value = "type", required = false, defaultValue = "NORMAL") type: GameMapTypeDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get map revisions", - nickname = "getMapRevisions", - notes = "Get list of all map revision IDs", - response = GameMapRevisionDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = GameMapRevisionDto::class, - responseContainer = "List" - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get the Map and image of the commit ID", + operationId = "getMapByCommitID", + description = "", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = MapCommitByCommitIdResponseDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user/map/revisions"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user/map/revision/{commitId}"], + produces = ["application/json"] ) - fun getMapRevisions(): ResponseEntity> { + fun getMapByCommitID(@Parameter(description = "", required = true) @PathVariable("commitId") commitId: java.util.UUID): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Update latest map", - nickname = "updateLatestMap", - notes = "Update latest map", - authorizations = [Authorization(value = "http-bearer")] + @Operation( + summary = "Get map revisions", + operationId = "getMapRevisions", + description = "Get list of all map revision IDs", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = GameMapRevisionDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @RequestMapping( + method = [RequestMethod.GET], + value = ["/user/map/revisions"], + produces = ["application/json"] + ) + fun getMapRevisions(@Parameter(description = "map type", schema = Schema(allowableValues = ["NORMAL", "DAILY_CHALLENGE"], defaultValue = "NORMAL")) @Valid @RequestParam(value = "type", required = false, defaultValue = "NORMAL") type: GameMapTypeDto): ResponseEntity> { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Update latest map", + operationId = "updateLatestMap", + description = "Update latest map", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/map/latest"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/map/latest"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun updateLatestMap( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody updateLatestMapRequestDto: UpdateLatestMapRequestDto - ): ResponseEntity { + fun updateLatestMap(@Parameter(description = "", required = true) @Valid @RequestBody updateLatestMapRequestDto: UpdateLatestMapRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/MatchApi.kt b/library/src/main/kotlin/delta/codecharacter/core/MatchApi.kt index 15c23ff2..e3f9136b 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/MatchApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/MatchApi.kt @@ -1,108 +1,91 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.CreateMatchRequestDto import delta.codecharacter.dtos.GenericErrorDto import delta.codecharacter.dtos.MatchDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Match", description = "The Match API") @RequestMapping("\${api.base-path:}") interface MatchApi { - @ApiOperation( - value = "Create match", - nickname = "createMatch", - notes = "Initiate a match by current user", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 201, - message = "Created" - ), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Create match", + operationId = "createMatch", + description = "Initiate a match by current user", + responses = [ + ApiResponse(responseCode = "201", description = "Created"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/user/matches"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/user/matches"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun createMatch( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody createMatchRequestDto: CreateMatchRequestDto - ): ResponseEntity { + fun createMatch(@Parameter(description = "", required = true) @Valid @RequestBody createMatchRequestDto: CreateMatchRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get top matches", - nickname = "getTopMatches", - notes = "Get top matches", - response = MatchDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = MatchDto::class, - responseContainer = "List" - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get top matches", + operationId = "getTopMatches", + description = "Get top matches", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = MatchDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/top-matches"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/top-matches"], + produces = ["application/json"] ) fun getTopMatches(): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get user matches", - nickname = "getUserMatches", - notes = "Get matches played by authenticated user", - response = MatchDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = MatchDto::class, - responseContainer = "List" - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Get user matches", + operationId = "getUserMatches", + description = "Get matches played by authenticated user", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = MatchDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user/matches"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user/matches"], + produces = ["application/json"] ) fun getUserMatches(): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) diff --git a/library/src/main/kotlin/delta/codecharacter/core/NotificationApi.kt b/library/src/main/kotlin/delta/codecharacter/core/NotificationApi.kt index 482a5a23..0c16cbdc 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/NotificationApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/NotificationApi.kt @@ -1,86 +1,72 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.GenericErrorDto import delta.codecharacter.dtos.NotificationDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "Notification", description = "The Notification API") @RequestMapping("\${api.base-path:}") interface NotificationApi { - @ApiOperation( - value = "Get all notifications", - nickname = "getAllNotifications", - notes = "Get all notifications", - response = NotificationDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = NotificationDto::class, - responseContainer = "List" - )] + @Operation( + summary = "Get all notifications", + operationId = "getAllNotifications", + description = "Get all notifications", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = NotificationDto::class))]) + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/user/notifications"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/user/notifications"], + produces = ["application/json"] ) fun getAllNotifications(): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Save notification read status", - nickname = "saveNotificationReadStatus", - notes = "Save notification read status", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse(code = 204, message = "No Content"), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse(code = 401, message = "Unauthorized")] + @Operation( + summary = "Save notification read status", + operationId = "saveNotificationReadStatus", + description = "Save notification read status", + responses = [ + ApiResponse(responseCode = "204", description = "No Content"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.PUT], - value = ["/user/notifications/{notificationId}/read"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.PUT], + value = ["/user/notifications/{notificationId}/read"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun saveNotificationReadStatus( - @ApiParam( - value = "ID of the notification", - required = true - ) @PathVariable("notificationId") notificationId: java.util.UUID, - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody body: Boolean - ): ResponseEntity { + fun saveNotificationReadStatus(@Parameter(description = "ID of the notification", required = true) @PathVariable("notificationId") notificationId: java.util.UUID,@Parameter(description = "", required = true) @Valid @RequestBody body: kotlin.Boolean): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/core/UserApi.kt b/library/src/main/kotlin/delta/codecharacter/core/UserApi.kt index e136f0ed..08acb3be 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/UserApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/UserApi.kt @@ -1,134 +1,96 @@ /** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1). + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.3.0). * https://openapi-generator.tech * Do not edit the class manually. - */ +*/ package delta.codecharacter.core import delta.codecharacter.dtos.ActivateUserRequestDto import delta.codecharacter.dtos.GenericErrorDto import delta.codecharacter.dtos.RatingHistoryDto import delta.codecharacter.dtos.RegisterUserRequestDto -import io.swagger.annotations.Api -import io.swagger.annotations.ApiOperation -import io.swagger.annotations.ApiParam -import io.swagger.annotations.ApiResponse -import io.swagger.annotations.ApiResponses -import io.swagger.annotations.Authorization +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import javax.validation.Valid +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.constraints.* +import jakarta.validation.Valid + +import kotlin.collections.List +import kotlin.collections.Map @Validated -@Api(value = "User", description = "The User API") @RequestMapping("\${api.base-path:}") interface UserApi { - @ApiOperation( - value = "Activate user", - nickname = "activateUser", - notes = "Activate user by using the token sent via email" - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK" - ), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - ), ApiResponse( - code = 401, - message = "Unauthorized" - ), ApiResponse(code = 404, message = "Not Found")] + @Operation( + summary = "Activate user", + operationId = "activateUser", + description = "Activate user by using the token sent via email", + responses = [ + ApiResponse(responseCode = "200", description = "OK"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized"), + ApiResponse(responseCode = "404", description = "Not Found") + ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/users/{userId}/activate"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/users/{userId}/activate"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun activateUser( - @ApiParam( - value = "ID of the user", - required = true - ) @PathVariable("userId") userId: java.util.UUID, - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody activateUserRequestDto: ActivateUserRequestDto - ): ResponseEntity { + fun activateUser(@Parameter(description = "ID of the user", required = true) @PathVariable("userId") userId: java.util.UUID,@Parameter(description = "", required = true) @Valid @RequestBody activateUserRequestDto: ActivateUserRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Get user rating history", - nickname = "getRatingHistory", - notes = "Get user rating history", - response = RatingHistoryDto::class, - responseContainer = "List", - authorizations = [Authorization(value = "http-bearer")] - ) - @ApiResponses( - value = [ApiResponse( - code = 200, - message = "OK", - response = RatingHistoryDto::class, - responseContainer = "List" - ), ApiResponse( - code = 401, - message = "Unauthorized" - ), ApiResponse( - code = 403, - message = "Forbidden" - ), ApiResponse(code = 404, message = "Not Found")] + @Operation( + summary = "Get user rating history", + operationId = "getRatingHistory", + description = "Get user rating history", + responses = [ + ApiResponse(responseCode = "200", description = "OK", content = [Content(schema = Schema(implementation = RatingHistoryDto::class))]), + ApiResponse(responseCode = "401", description = "Unauthorized"), + ApiResponse(responseCode = "403", description = "Forbidden"), + ApiResponse(responseCode = "404", description = "Not Found") + ], + security = [ SecurityRequirement(name = "http-bearer") ] ) @RequestMapping( - method = [RequestMethod.GET], - value = ["/users/{userId}/ratingHistory"], - produces = ["application/json"] + method = [RequestMethod.GET], + value = ["/users/{userId}/ratingHistory"], + produces = ["application/json"] ) - fun getRatingHistory( - @ApiParam( - value = "ID of the user", - required = true - ) @PathVariable("userId") userId: java.util.UUID - ): ResponseEntity> { + fun getRatingHistory(@Parameter(description = "ID of the user", required = true) @PathVariable("userId") userId: java.util.UUID): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @ApiOperation( - value = "Register user", - nickname = "register", - notes = "Register user" - ) - @ApiResponses( - value = [ApiResponse( - code = 201, - message = "Created" - ), ApiResponse( - code = 400, - message = "Bad Request", - response = GenericErrorDto::class - )] + @Operation( + summary = "Register user", + operationId = "register", + description = "Register user", + responses = [ + ApiResponse(responseCode = "201", description = "Created"), + ApiResponse(responseCode = "400", description = "Bad Request", content = [Content(schema = Schema(implementation = GenericErrorDto::class))]) + ] ) @RequestMapping( - method = [RequestMethod.POST], - value = ["/users"], - produces = ["application/json"], - consumes = ["application/json"] + method = [RequestMethod.POST], + value = ["/users"], + produces = ["application/json"], + consumes = ["application/json"] ) - fun register( - @ApiParam( - value = "", - required = true - ) @Valid @RequestBody registerUserRequestDto: RegisterUserRequestDto - ): ResponseEntity { + fun register(@Parameter(description = "", required = true) @Valid @RequestBody registerUserRequestDto: RegisterUserRequestDto): ResponseEntity { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/ActivateUserRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/ActivateUserRequestDto.kt index debc9a56..29a9efbf 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/ActivateUserRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/ActivateUserRequestDto.kt @@ -1,14 +1,20 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Activate user request - * @param token + * @param token */ data class ActivateUserRequestDto( - @ApiModelProperty(example = "example-token", required = true, value = "") - @field:JsonProperty("token", required = true) val token: String -) + @Schema(example = "example-token", required = true, description = "") + @get:JsonProperty("token", required = true) val token: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/AuthStatusResponseDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/AuthStatusResponseDto.kt index 77dedfb3..fb848fd3 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/AuthStatusResponseDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/AuthStatusResponseDto.kt @@ -1,31 +1,32 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** - * - * @param status + * + * @param status */ data class AuthStatusResponseDto( - @ApiModelProperty(example = "null", value = "") - @field:JsonProperty("status") val status: Status? = null + @Schema(example = "null", description = "") + @get:JsonProperty("status") val status: AuthStatusResponseDto.Status? = null ) { /** - * - * Values: AUTHENTICATED,PROFILE_INCOMPLETE,ACTIVATION_PENDING - */ - enum class Status(val value: String) { + * + * Values: AUTHENTICATED,PROFILE_INCOMPLETE,ACTIVATION_PENDING + */ + enum class Status(val value: kotlin.String) { - @JsonProperty("AUTHENTICATED") - AUTHENTICATED("AUTHENTICATED"), - - @JsonProperty("PROFILE_INCOMPLETE") - PROFILE_INCOMPLETE("PROFILE_INCOMPLETE"), - - @JsonProperty("ACTIVATION_PENDING") - ACTIVATION_PENDING("ACTIVATION_PENDING"); + @JsonProperty("AUTHENTICATED") AUTHENTICATED("AUTHENTICATED"), + @JsonProperty("PROFILE_INCOMPLETE") PROFILE_INCOMPLETE("PROFILE_INCOMPLETE"), + @JsonProperty("ACTIVATION_PENDING") ACTIVATION_PENDING("ACTIVATION_PENDING") } + } + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/ChallengeTypeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/ChallengeTypeDto.kt new file mode 100644 index 00000000..630ca6ba --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/ChallengeTypeDto.kt @@ -0,0 +1,19 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** +* +* Values: CODE,MAP +*/ +enum class ChallengeTypeDto(val value: kotlin.String) { + + @JsonProperty("CODE") CODE("CODE"), + @JsonProperty("MAP") MAP("MAP") +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CodeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CodeDto.kt index ab87f3a6..fe5afe38 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CodeDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CodeDto.kt @@ -1,34 +1,31 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.LanguageDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Code model - * @param code - * @param lastSavedAt - * @param language + * @param code + * @param lastSavedAt + * @param language */ data class CodeDto( - @ApiModelProperty( - example = "#include ", - required = true, - value = "" - ) - @field:JsonProperty("code", required = true) val code: String, + @Schema(example = "#include ", required = true, description = "") + @get:JsonProperty("code", required = true) val code: kotlin.String, - @ApiModelProperty( - required = true, - value = "" - ) - @field:JsonProperty( - "lastSavedAt", - required = true - ) val lastSavedAt: java.time.Instant, + @Schema(example = "2021-01-01T00:00Z", required = true, description = "") + @get:JsonProperty("lastSavedAt", required = true) val lastSavedAt: java.time.Instant, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("language", required = true) val language: LanguageDto -) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("language", required = true) val language: LanguageDto +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CodeRevisionDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CodeRevisionDto.kt index 3bf0aa3a..107d0217 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CodeRevisionDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CodeRevisionDto.kt @@ -1,50 +1,43 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.LanguageDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Code revision model - * @param id - * @param code - * @param message - * @param language - * @param createdAt - * @param parentRevision + * @param id + * @param code + * @param message + * @param language + * @param createdAt + * @param parentRevision */ data class CodeRevisionDto( - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174000", - required = true, - value = "" - ) - @field:JsonProperty("id", required = true) val id: java.util.UUID, + @Schema(example = "123e4567-e89b-12d3-a456-426614174000", required = true, description = "") + @get:JsonProperty("id", required = true) val id: java.util.UUID, - @ApiModelProperty( - example = "#include ", - required = true, - value = "" - ) - @field:JsonProperty("code", required = true) val code: String, + @Schema(example = "#include ", required = true, description = "") + @get:JsonProperty("code", required = true) val code: kotlin.String, - @ApiModelProperty(example = "message", required = true, value = "") - @field:JsonProperty("message", required = true) val message: String, + @Schema(example = "message", required = true, description = "") + @get:JsonProperty("message", required = true) val message: kotlin.String, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("language", required = true) val language: LanguageDto, - - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "createdAt", - required = true - ) val createdAt: java.time.Instant, - - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174111", - value = "" - ) - @field:JsonProperty("parentRevision") val parentRevision: java.util.UUID? = null -) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("language", required = true) val language: LanguageDto, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("createdAt", required = true) val createdAt: java.time.Instant, + + @Schema(example = "123e4567-e89b-12d3-a456-426614174111", description = "") + @get:JsonProperty("parentRevision") val parentRevision: java.util.UUID? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CodeTypeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CodeTypeDto.kt new file mode 100644 index 00000000..bc491790 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CodeTypeDto.kt @@ -0,0 +1,19 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** +* +* Values: NORMAL,DAILY_CHALLENGE +*/ +enum class CodeTypeDto(val value: kotlin.String) { + + @JsonProperty("NORMAL") NORMAL("NORMAL"), + @JsonProperty("DAILY_CHALLENGE") DAILY_CHALLENGE("DAILY_CHALLENGE") +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CompleteProfileRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CompleteProfileRequestDto.kt index 86096ee9..0115bf73 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CompleteProfileRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CompleteProfileRequestDto.kt @@ -1,33 +1,36 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Model for complete profile request - * @param username - * @param name - * @param country - * @param college - * @param avatarId + * @param username + * @param name + * @param country + * @param college + * @param avatarId */ data class CompleteProfileRequestDto( - @ApiModelProperty(example = "TestUser", required = true, value = "") - @field:JsonProperty( - "username", - required = true - ) val username: String, + @Schema(example = "TestUser", required = true, description = "") + @get:JsonProperty("username", required = true) val username: kotlin.String, - @ApiModelProperty(example = "Test User", required = true, value = "") - @field:JsonProperty("name", required = true) val name: String, + @Schema(example = "Test User", required = true, description = "") + @get:JsonProperty("name", required = true) val name: kotlin.String, - @ApiModelProperty(example = "IN", required = true, value = "") - @field:JsonProperty("country", required = true) val country: String, + @Schema(example = "IN", required = true, description = "") + @get:JsonProperty("country", required = true) val country: kotlin.String, - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("college", required = true) val college: String, + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("college", required = true) val college: kotlin.String, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("avatarId", required = true) val avatarId: kotlin.Int +) { + +} - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("avatarId", required = true) val avatarId: Int -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CreateCodeRevisionRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CreateCodeRevisionRequestDto.kt index bc5624cf..36364c2d 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CreateCodeRevisionRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CreateCodeRevisionRequestDto.kt @@ -1,28 +1,37 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.CodeTypeDto +import delta.codecharacter.dtos.LanguageDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Create code revision request - * @param code - * @param message - * @param language + * @param code + * @param message + * @param language + * @param codeType */ data class CreateCodeRevisionRequestDto( - @ApiModelProperty( - example = "#include ", - required = true, - value = "" - ) - @field:JsonProperty("code", required = true) val code: String, + @Schema(example = "#include ", required = true, description = "") + @get:JsonProperty("code", required = true) val code: kotlin.String, - @ApiModelProperty(example = "message", required = true, value = "") - @field:JsonProperty("message", required = true) val message: String, + @Schema(example = "message", required = true, description = "") + @get:JsonProperty("message", required = true) val message: kotlin.String, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("language", required = true) val language: LanguageDto -) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("language", required = true) val language: LanguageDto, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("codeType") val codeType: CodeTypeDto? = CodeTypeDto.NORMAL +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CreateMapRevisionRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CreateMapRevisionRequestDto.kt index 8a8c7755..d5395e14 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CreateMapRevisionRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CreateMapRevisionRequestDto.kt @@ -1,22 +1,35 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.GameMapTypeDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Create map revision request - * @param map - * @param message + * @param map + * @param mapImage + * @param message + * @param mapType */ data class CreateMapRevisionRequestDto( - @ApiModelProperty( - example = "0000\n0010\n0100\n1000\n", - required = true, - value = "" - ) - @field:JsonProperty("map", required = true) val map: String, + @Schema(example = "0000\n0010\n0100\n1000\n", required = true, description = "") + @get:JsonProperty("map", required = true) val map: kotlin.String, + + @Schema(example = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII", required = true, description = "") + @get:JsonProperty("mapImage", required = true) val mapImage: kotlin.String, + + @Schema(example = "message", required = true, description = "") + @get:JsonProperty("message", required = true) val message: kotlin.String, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("mapType") val mapType: GameMapTypeDto? = GameMapTypeDto.NORMAL +) { + +} - @ApiModelProperty(example = "message", required = true, value = "") - @field:JsonProperty("message", required = true) val message: String -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CreateMatchRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CreateMatchRequestDto.kt index 51c876a7..186e5ac9 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CreateMatchRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CreateMatchRequestDto.kt @@ -1,12 +1,16 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.MatchModeDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Create match request If mode is SELF: either/both of mapRevisionId and codeRevisionId have to be provided, or else latest code will be used to initiate the match If mode is MANUAL: only opponentUsername should be provided - * @param mode + * @param mode * @param opponentUsername Username of the opponent * @param mapRevisionId Revision ID of the map * @param codeRevisionId Revision of the code @@ -14,15 +18,18 @@ import javax.validation.Valid data class CreateMatchRequestDto( @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("mode", required = true) val mode: MatchModeDto, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("mode", required = true) val mode: MatchModeDto, - @ApiModelProperty(example = "null", value = "Username of the opponent") - @field:JsonProperty("opponentUsername") val opponentUsername: String? = null, + @Schema(example = "null", description = "Username of the opponent") + @get:JsonProperty("opponentUsername") val opponentUsername: kotlin.String? = null, - @ApiModelProperty(example = "null", value = "Revision ID of the map") - @field:JsonProperty("mapRevisionId") val mapRevisionId: java.util.UUID? = null, + @Schema(example = "null", description = "Revision ID of the map") + @get:JsonProperty("mapRevisionId") val mapRevisionId: java.util.UUID? = null, + + @Schema(example = "null", description = "Revision of the code") + @get:JsonProperty("codeRevisionId") val codeRevisionId: java.util.UUID? = null +) { + +} - @ApiModelProperty(example = "null", value = "Revision of the code") - @field:JsonProperty("codeRevisionId") val codeRevisionId: java.util.UUID? = null -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/CurrentUserProfileDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/CurrentUserProfileDto.kt index 46f2ffc6..33f7b245 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/CurrentUserProfileDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/CurrentUserProfileDto.kt @@ -1,53 +1,65 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.constraints.Pattern +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.TierTypeDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Current user profile model - * @param id - * @param username - * @param name - * @param email - * @param country - * @param college - * @param avatarId - * @param isProfileComplete + * @param id + * @param username + * @param name + * @param email + * @param country + * @param college + * @param avatarId + * @param tutorialLevel + * @param isProfileComplete + * @param isTutorialComplete + * @param tier */ data class CurrentUserProfileDto( - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174003", - required = true, - value = "" - ) - @field:JsonProperty("id", required = true) val id: java.util.UUID, - - @ApiModelProperty(example = "test", required = true, value = "") - @field:JsonProperty( - "username", - required = true - ) val username: String, - - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("name", required = true) val name: String, - @get:Pattern(regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") - @ApiModelProperty(example = "test@test.com", required = true, value = "") - @field:JsonProperty("email", required = true) val email: String, - - @ApiModelProperty(example = "IN", required = true, value = "") - @field:JsonProperty("country", required = true) val country: String, - - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("college", required = true) val college: String, - - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("avatarId", required = true) val avatarId: Int, - - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "isProfileComplete", - required = true - ) val isProfileComplete: Boolean = false -) + @Schema(example = "123e4567-e89b-12d3-a456-426614174003", required = true, description = "") + @get:JsonProperty("id", required = true) val id: java.util.UUID, + + @Schema(example = "test", required = true, description = "") + @get:JsonProperty("username", required = true) val username: kotlin.String, + + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @get:Email + @get:Pattern(regexp="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") + @Schema(example = "test@test.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @Schema(example = "IN", required = true, description = "") + @get:JsonProperty("country", required = true) val country: kotlin.String, + + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("college", required = true) val college: kotlin.String, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("avatarId", required = true) val avatarId: kotlin.Int, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("tutorialLevel", required = true) val tutorialLevel: kotlin.Int, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("isProfileComplete", required = true) val isProfileComplete: kotlin.Boolean = false, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("isTutorialComplete", required = true) val isTutorialComplete: kotlin.Boolean = false, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("tier") val tier: TierTypeDto? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeGetRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeGetRequestDto.kt new file mode 100644 index 00000000..0aff9511 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeGetRequestDto.kt @@ -0,0 +1,41 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.dtos.DailyChallengeObjectDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * Get current-user daily challenge + * @param challName + * @param chall + * @param challType + * @param description + * @param completionStatus + */ +data class DailyChallengeGetRequestDto( + + @Schema(example = "Daily Challenge 1", required = true, description = "") + @get:JsonProperty("challName", required = true) val challName: kotlin.String, + + @field:Valid + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("chall", required = true) val chall: DailyChallengeObjectDto, + + @field:Valid + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("challType", required = true) val challType: ChallengeTypeDto, + + @Schema(example = "Daily Challenge description", description = "") + @get:JsonProperty("description") val description: kotlin.String? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("completionStatus") val completionStatus: kotlin.Boolean? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeLeaderBoardResponseDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeLeaderBoardResponseDto.kt new file mode 100644 index 00000000..4b131a5c --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeLeaderBoardResponseDto.kt @@ -0,0 +1,28 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * Response model for daily challenge leaderboard + * @param userName + * @param score + * @param avatarId + */ +data class DailyChallengeLeaderBoardResponseDto( + + @Schema(example = "TestUser", required = true, description = "") + @get:JsonProperty("userName", required = true) val userName: kotlin.String, + + @Schema(example = "1500.0", required = true, description = "") + @get:JsonProperty("score", required = true) val score: java.math.BigDecimal, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("avatarId", required = true) val avatarId: kotlin.Int +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeMatchRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeMatchRequestDto.kt new file mode 100644 index 00000000..0afb0dba --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeMatchRequestDto.kt @@ -0,0 +1,27 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.LanguageDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * Request Model for the daily challenge + * @param `value` + * @param language + */ +data class DailyChallengeMatchRequestDto( + + @Schema(example = "#include", required = true, description = "") + @get:JsonProperty("value", required = true) val `value`: kotlin.String, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("language") val language: LanguageDto? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeObjectDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeObjectDto.kt new file mode 100644 index 00000000..3d128b62 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/DailyChallengeObjectDto.kt @@ -0,0 +1,32 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * The object describing the challenge for the day + * @param cpp + * @param java + * @param python + * @param image + */ +data class DailyChallengeObjectDto( + + @Schema(example = "null", description = "") + @get:JsonProperty("cpp") val cpp: kotlin.String? = null, + + @Schema(example = "null", description = "") + @get:JsonProperty("java") val java: kotlin.String? = null, + + @Schema(example = "null", description = "") + @get:JsonProperty("python") val python: kotlin.String? = null, + + @Schema(example = "null", description = "") + @get:JsonProperty("image") val image: kotlin.String? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/ForgotPasswordRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/ForgotPasswordRequestDto.kt index 130d09d6..959d3f13 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/ForgotPasswordRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/ForgotPasswordRequestDto.kt @@ -1,15 +1,26 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.constraints.Pattern +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Forgot password request - * @param email + * @param email + * @param recaptchaCode */ data class ForgotPasswordRequestDto( - @get:Pattern(regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") - @ApiModelProperty(example = "test@test.com", required = true, value = "") - @field:JsonProperty("email", required = true) val email: String -) + + @get:Email + @get:Pattern(regexp="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") + @Schema(example = "test@test.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @Schema(example = "example recaptcha-code", description = "") + @get:JsonProperty("recaptchaCode") val recaptchaCode: kotlin.String? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/GameDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/GameDto.kt index 66012de8..71e07eb0 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/GameDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/GameDto.kt @@ -1,35 +1,35 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.GameStatusDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Game model - * @param id - * @param destruction - * @param coinsUsed - * @param status + * @param id + * @param destruction + * @param coinsUsed + * @param status */ data class GameDto( - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174000", - required = true, - value = "" - ) - @field:JsonProperty("id", required = true) val id: java.util.UUID, + @Schema(example = "123e4567-e89b-12d3-a456-426614174000", required = true, description = "") + @get:JsonProperty("id", required = true) val id: java.util.UUID, - @ApiModelProperty(example = "100", required = true, value = "") - @field:JsonProperty( - "destruction", - required = true - ) val destruction: java.math.BigDecimal, + @Schema(example = "100", required = true, description = "") + @get:JsonProperty("destruction", required = true) val destruction: java.math.BigDecimal, - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("coinsUsed", required = true) val coinsUsed: Int, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("coinsUsed", required = true) val coinsUsed: kotlin.Int, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("status", required = true) val status: GameStatusDto -) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("status", required = true) val status: GameStatusDto +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/GameMapDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/GameMapDto.kt index 2ab70d65..97bc4632 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/GameMapDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/GameMapDto.kt @@ -1,28 +1,28 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * GameMap model - * @param map - * @param lastSavedAt + * @param map + * @param mapImage + * @param lastSavedAt */ data class GameMapDto( - @ApiModelProperty( - example = "0000\n0010\n0100\n1000\n", - required = true, - value = "" - ) - @field:JsonProperty("map", required = true) val map: String, + @Schema(example = "0000\n0010\n0100\n1000\n", required = true, description = "") + @get:JsonProperty("map", required = true) val map: kotlin.String, + + @Schema(example = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII", required = true, description = "") + @get:JsonProperty("mapImage", required = true) val mapImage: kotlin.String, + + @Schema(example = "2021-01-01T00:00Z", required = true, description = "") + @get:JsonProperty("lastSavedAt", required = true) val lastSavedAt: java.time.Instant +) { + +} - @ApiModelProperty( - required = true, - value = "" - ) - @field:JsonProperty( - "lastSavedAt", - required = true - ) val lastSavedAt: java.time.Instant -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/GameMapRevisionDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/GameMapRevisionDto.kt index a34889cb..1f350008 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/GameMapRevisionDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/GameMapRevisionDto.kt @@ -1,44 +1,36 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * GameMap revision model - * @param id - * @param map - * @param createdAt - * @param message - * @param parentRevision + * @param id + * @param map + * @param createdAt + * @param message + * @param parentRevision */ data class GameMapRevisionDto( - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174000", - required = true, - value = "" - ) - @field:JsonProperty("id", required = true) val id: java.util.UUID, - - @ApiModelProperty( - example = "0000\n0010\n0100\n1000\n", - required = true, - value = "" - ) - @field:JsonProperty("map", required = true) val map: String, - - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "createdAt", - required = true - ) val createdAt: java.time.Instant, - - @ApiModelProperty(example = "message", required = true, value = "") - @field:JsonProperty("message", required = true) val message: String, - - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174111", - value = "" - ) - @field:JsonProperty("parentRevision") val parentRevision: java.util.UUID? = null -) + @Schema(example = "123e4567-e89b-12d3-a456-426614174000", required = true, description = "") + @get:JsonProperty("id", required = true) val id: java.util.UUID, + + @Schema(example = "0000\n0010\n0100\n1000\n", required = true, description = "") + @get:JsonProperty("map", required = true) val map: kotlin.String, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("createdAt", required = true) val createdAt: java.time.Instant, + + @Schema(example = "message", required = true, description = "") + @get:JsonProperty("message", required = true) val message: kotlin.String, + + @Schema(example = "123e4567-e89b-12d3-a456-426614174111", description = "") + @get:JsonProperty("parentRevision") val parentRevision: java.util.UUID? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/GameMapTypeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/GameMapTypeDto.kt new file mode 100644 index 00000000..95b31a75 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/GameMapTypeDto.kt @@ -0,0 +1,19 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** +* +* Values: NORMAL,DAILY_CHALLENGE +*/ +enum class GameMapTypeDto(val value: kotlin.String) { + + @JsonProperty("NORMAL") NORMAL("NORMAL"), + @JsonProperty("DAILY_CHALLENGE") DAILY_CHALLENGE("DAILY_CHALLENGE") +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/GameStatusDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/GameStatusDto.kt index a9ec6579..6178a3b7 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/GameStatusDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/GameStatusDto.kt @@ -1,22 +1,21 @@ package delta.codecharacter.dtos +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** - * - * Values: IDLE,EXECUTING,EXECUTED,EXECUTE_ERROR - */ -enum class GameStatusDto(val value: String) { +* +* Values: IDLE,EXECUTING,EXECUTED,EXECUTE_ERROR +*/ +enum class GameStatusDto(val value: kotlin.String) { - @JsonProperty("IDLE") - IDLE("IDLE"), - - @JsonProperty("EXECUTING") - EXECUTING("EXECUTING"), - - @JsonProperty("EXECUTED") - EXECUTED("EXECUTED"), - - @JsonProperty("EXECUTE_ERROR") - EXECUTE_ERROR("EXECUTE_ERROR"); + @JsonProperty("IDLE") IDLE("IDLE"), + @JsonProperty("EXECUTING") EXECUTING("EXECUTING"), + @JsonProperty("EXECUTED") EXECUTED("EXECUTED"), + @JsonProperty("EXECUTE_ERROR") EXECUTE_ERROR("EXECUTE_ERROR") } + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/GenericErrorDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/GenericErrorDto.kt index 796172ca..19251f6f 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/GenericErrorDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/GenericErrorDto.kt @@ -1,14 +1,20 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Model for Generic Error - * @param message + * @param message */ data class GenericErrorDto( - @ApiModelProperty(example = "null", value = "") - @field:JsonProperty("message") val message: String? = null -) + @Schema(example = "null", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/LanguageDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/LanguageDto.kt index 0faaa01e..c391d8de 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/LanguageDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/LanguageDto.kt @@ -1,22 +1,21 @@ package delta.codecharacter.dtos +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** - * Language of source files - * Values: C,CPP,JAVA,PYTHON - */ -enum class LanguageDto(val value: String) { +* Language of source files +* Values: C,CPP,JAVA,PYTHON +*/ +enum class LanguageDto(val value: kotlin.String) { - @JsonProperty("C") - C("C"), - - @JsonProperty("CPP") - CPP("CPP"), - - @JsonProperty("JAVA") - JAVA("JAVA"), - - @JsonProperty("PYTHON") - PYTHON("PYTHON"); + @JsonProperty("C") C("C"), + @JsonProperty("CPP") CPP("CPP"), + @JsonProperty("JAVA") JAVA("JAVA"), + @JsonProperty("PYTHON") PYTHON("PYTHON") } + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/LeaderboardEntryDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/LeaderboardEntryDto.kt index fae1673b..9f97b67a 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/LeaderboardEntryDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/LeaderboardEntryDto.kt @@ -1,21 +1,28 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import delta.codecharacter.dtos.PublicUserDto +import delta.codecharacter.dtos.UserStatsDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Leaderboard entry model - * @param user - * @param stats + * @param user + * @param stats */ data class LeaderboardEntryDto( @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("user", required = true) val user: PublicUserDto, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("user", required = true) val user: PublicUserDto, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("stats", required = true) val stats: UserStatsDto -) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("stats", required = true) val stats: UserStatsDto +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/MapCommitByCommitIdResponseDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/MapCommitByCommitIdResponseDto.kt new file mode 100644 index 00000000..fc27de01 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/MapCommitByCommitIdResponseDto.kt @@ -0,0 +1,24 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * Get map image and map by commitId + * @param mapImage + * @param map + */ +data class MapCommitByCommitIdResponseDto( + + @Schema(example = "base-64-string", required = true, description = "") + @get:JsonProperty("mapImage", required = true) val mapImage: kotlin.String, + + @Schema(example = "0000\n0000\n0001\n0000", required = true, description = "") + @get:JsonProperty("map", required = true) val map: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/MatchDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/MatchDto.kt index 4471950e..0772b8b1 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/MatchDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/MatchDto.kt @@ -1,63 +1,54 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.GameDto +import delta.codecharacter.dtos.MatchModeDto +import delta.codecharacter.dtos.PublicUserDto +import delta.codecharacter.dtos.VerdictDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Match model - * @param id - * @param games - * @param matchMode - * @param matchVerdict - * @param createdAt - * @param user1 - * @param user2 + * @param id + * @param games + * @param matchMode + * @param matchVerdict + * @param createdAt + * @param user1 + * @param user2 */ data class MatchDto( - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174000", - required = true, - value = "" - ) - @field:JsonProperty("id", required = true) val id: java.util.UUID, + @Schema(example = "123e4567-e89b-12d3-a456-426614174000", required = true, description = "") + @get:JsonProperty("id", required = true) val id: java.util.UUID, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "games", - required = true - ) val games: Set, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("games", required = true) val games: kotlin.collections.Set, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "matchMode", - required = true - ) val matchMode: MatchModeDto, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("matchMode", required = true) val matchMode: MatchModeDto, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "matchVerdict", - required = true - ) val matchVerdict: VerdictDto, - - @ApiModelProperty( - required = true, - value = "" - ) - @field:JsonProperty( - "createdAt", - required = true - ) val createdAt: java.time.Instant, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("matchVerdict", required = true) val matchVerdict: VerdictDto, + + @Schema(example = "2021-01-01T00:00Z", required = true, description = "") + @get:JsonProperty("createdAt", required = true) val createdAt: java.time.Instant, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("user1", required = true) val user1: PublicUserDto, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("user1", required = true) val user1: PublicUserDto, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("user2", required = true) val user2: PublicUserDto -) + @Schema(example = "null", description = "") + @get:JsonProperty("user2") val user2: PublicUserDto? = null +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt index 0bc41bac..5d631ca5 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt @@ -1,19 +1,21 @@ package delta.codecharacter.dtos +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** - * Match Mode - * Values: SELF,MANUAL,AUTO - */ -enum class MatchModeDto(val value: String) { +* Match Mode +* Values: SELF,MANUAL,AUTO,DAILYCHALLENGE +*/ +enum class MatchModeDto(val value: kotlin.String) { - @JsonProperty("SELF") - SELF("SELF"), - - @JsonProperty("MANUAL") - MANUAL("MANUAL"), - - @JsonProperty("AUTO") - AUTO("AUTO"); + @JsonProperty("SELF") SELF("SELF"), + @JsonProperty("MANUAL") MANUAL("MANUAL"), + @JsonProperty("AUTO") AUTO("AUTO"), + @JsonProperty("DAILYCHALLENGE") DAILYCHALLENGE("DAILYCHALLENGE") } + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/NotificationDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/NotificationDto.kt index 922674d7..3a6e2bb6 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/NotificationDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/NotificationDto.kt @@ -1,47 +1,36 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Notification model - * @param id - * @param title - * @param content - * @param createdAt - * @param read + * @param id + * @param title + * @param content + * @param createdAt + * @param read */ data class NotificationDto( - @ApiModelProperty( - example = "123e4567-e89b-12d3-a456-426614174000", - required = true, - value = "" - ) - @field:JsonProperty("id", required = true) val id: java.util.UUID, - - @ApiModelProperty( - example = "Test notification", - required = true, - value = "" - ) - @field:JsonProperty("title", required = true) val title: String, - - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("content", required = true) val content: String, - - @ApiModelProperty( - required = true, - value = "" - ) - @field:JsonProperty( - "createdAt", - required = true - ) val createdAt: java.time.Instant, - - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "read", - required = true - ) val read: Boolean = false -) + @Schema(example = "123e4567-e89b-12d3-a456-426614174000", required = true, description = "") + @get:JsonProperty("id", required = true) val id: java.util.UUID, + + @Schema(example = "Test notification", required = true, description = "") + @get:JsonProperty("title", required = true) val title: kotlin.String, + + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("content", required = true) val content: kotlin.String, + + @Schema(example = "2021-01-01T00:00Z", required = true, description = "") + @get:JsonProperty("createdAt", required = true) val createdAt: java.time.Instant, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("read", required = true) val read: kotlin.Boolean = false +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginRequestDto.kt index d298c874..95221459 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginRequestDto.kt @@ -1,21 +1,28 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.constraints.Pattern -import javax.validation.constraints.Size +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Password Login request - * @param email - * @param password + * @param email + * @param password */ data class PasswordLoginRequestDto( - @get:Pattern(regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") - @ApiModelProperty(example = "test@test.com", required = true, value = "") - @field:JsonProperty("email", required = true) val email: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("password", required = true) val password: String -) + + @get:Email + @get:Pattern(regexp="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") + @Schema(example = "test@test.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("password", required = true) val password: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginResponseDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginResponseDto.kt index c77d46fb..dfef3639 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginResponseDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/PasswordLoginResponseDto.kt @@ -1,7 +1,10 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Login response with user token @@ -9,10 +12,9 @@ import io.swagger.annotations.ApiModelProperty */ data class PasswordLoginResponseDto( - @ApiModelProperty( - example = "test-token", - required = true, - value = "Bearer token" - ) - @field:JsonProperty("token", required = true) val token: String -) + @Schema(example = "test-token", required = true, description = "Bearer token") + @get:JsonProperty("token", required = true) val token: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/PublicUserDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/PublicUserDto.kt index f232ec40..009dc403 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/PublicUserDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/PublicUserDto.kt @@ -1,33 +1,43 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.TierTypeDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Public user model - * @param username - * @param name - * @param country - * @param college - * @param avatarId + * @param username + * @param name + * @param country + * @param tier + * @param college + * @param avatarId */ data class PublicUserDto( - @ApiModelProperty(example = "test", required = true, value = "") - @field:JsonProperty( - "username", - required = true - ) val username: String, + @Schema(example = "test", required = true, description = "") + @get:JsonProperty("username", required = true) val username: kotlin.String, - @ApiModelProperty(example = "Test User", required = true, value = "") - @field:JsonProperty("name", required = true) val name: String, + @Schema(example = "Test User", required = true, description = "") + @get:JsonProperty("name", required = true) val name: kotlin.String, - @ApiModelProperty(example = "IN", required = true, value = "") - @field:JsonProperty("country", required = true) val country: String, + @Schema(example = "IN", required = true, description = "") + @get:JsonProperty("country", required = true) val country: kotlin.String, - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("college", required = true) val college: String, + @field:Valid + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("tier", required = true) val tier: TierTypeDto, + + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("college", required = true) val college: kotlin.String, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("avatarId", required = true) val avatarId: kotlin.Int +) { + +} - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("avatarId", required = true) val avatarId: Int -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/RatingHistoryDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/RatingHistoryDto.kt index b01ecda5..3a83c6ae 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/RatingHistoryDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/RatingHistoryDto.kt @@ -1,34 +1,28 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Rating history model - * @param rating - * @param ratingDeviation - * @param validFrom + * @param rating + * @param ratingDeviation + * @param validFrom */ data class RatingHistoryDto( - @ApiModelProperty(example = "1000", required = true, value = "") - @field:JsonProperty( - "rating", - required = true - ) val rating: java.math.BigDecimal, + @Schema(example = "1000", required = true, description = "") + @get:JsonProperty("rating", required = true) val rating: java.math.BigDecimal, - @ApiModelProperty(example = "5", required = true, value = "") - @field:JsonProperty( - "ratingDeviation", - required = true - ) val ratingDeviation: java.math.BigDecimal, + @Schema(example = "5", required = true, description = "") + @get:JsonProperty("ratingDeviation", required = true) val ratingDeviation: java.math.BigDecimal, + + @Schema(example = "2021-01-01T00:00Z", required = true, description = "") + @get:JsonProperty("validFrom", required = true) val validFrom: java.time.Instant +) { + +} - @ApiModelProperty( - required = true, - value = "" - ) - @field:JsonProperty( - "validFrom", - required = true - ) val validFrom: java.time.Instant -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/RegisterUserRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/RegisterUserRequestDto.kt index 4f62282c..d654e5f3 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/RegisterUserRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/RegisterUserRequestDto.kt @@ -1,55 +1,58 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.constraints.Pattern -import javax.validation.constraints.Size +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Register user request - * @param username - * @param name - * @param email - * @param password - * @param passwordConfirmation - * @param country - * @param college - * @param avatarId + * @param username + * @param name + * @param email + * @param password + * @param passwordConfirmation + * @param country + * @param college + * @param avatarId + * @param recaptchaCode */ data class RegisterUserRequestDto( - @ApiModelProperty(example = "test", required = true, value = "") - @field:JsonProperty( - "username", - required = true - ) val username: String, - - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("name", required = true) val name: String, - @get:Pattern(regexp = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") - @ApiModelProperty(example = "test@test.com", required = true, value = "") - @field:JsonProperty("email", required = true) val email: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "password", - required = true - ) val password: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "passwordConfirmation", - required = true - ) val passwordConfirmation: String, - - @ApiModelProperty(example = "IN", required = true, value = "") - @field:JsonProperty("country", required = true) val country: String, - - @ApiModelProperty(example = "Test", required = true, value = "") - @field:JsonProperty("college", required = true) val college: String, - - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("avatarId", required = true) val avatarId: Int -) + @Schema(example = "test", required = true, description = "") + @get:JsonProperty("username", required = true) val username: kotlin.String, + + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @get:Email + @get:Pattern(regexp="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}") + @Schema(example = "test@test.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("password", required = true) val password: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("passwordConfirmation", required = true) val passwordConfirmation: kotlin.String, + + @Schema(example = "IN", required = true, description = "") + @get:JsonProperty("country", required = true) val country: kotlin.String, + + @Schema(example = "Test", required = true, description = "") + @get:JsonProperty("college", required = true) val college: kotlin.String, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("avatarId", required = true) val avatarId: kotlin.Int, + + @Schema(example = "03AD1IbLAGl_UdwYP3-AeibnfJgXy_g3cNr_rhkBBh4zalD9GEXAR2xCcUGi7WlxFgOjYlpbRpZFTJJDVugJF-H4pBl32DU619cYHplp_ReGiOokgvz8DwiRLIZBvg1eu2e77jihWQPndoWU_WOTKrYVq1mzBcdPUfJ3PEMCo-eGvoyRaNvRWE0JYBSBgDfwFBaw8RmxaqiS84or-_G7-TDiifFYpcNFiIolIjGi9DkbMXivkjiIoEomAz6NUHg0alrk0C5_p1maoErBmpwLGwlAgKL_sa-ZAzHb89OprdVI8BXtN0CATBgwYO6u_zqrK5N9wDQyh-OmtFh5RXkEzmkASls33UYcJrtMfeFU-b9N-u-Je6NXVfkX49gAGan3k-GqkgcFKHowc5Cwym9tlGLrfiBtqKLIADw1UX4BCbIx9BbHlesoKEubr7MoVZCDv3VfctSTMXG-oH5IbDRQhez4E6JHR4Uv0lWyHKROv7wdxqXauz5PBlUlE11BdffXU5NEssJkM4Tk3zg5k6ddkju8DU2keqXodnzXVTBIXC6zxriA8IHaS_KFBtazAYZ6oac3-5Y2VMwli3XaADBCCVJzXC0GTa1jeuZQ", required = true, description = "") + @get:JsonProperty("recaptchaCode", required = true) val recaptchaCode: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/ResetPasswordRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/ResetPasswordRequestDto.kt index 1680d723..2da19751 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/ResetPasswordRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/ResetPasswordRequestDto.kt @@ -1,32 +1,32 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.constraints.Pattern -import javax.validation.constraints.Size +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Reset password request - * @param token - * @param password - * @param passwordConfirmation + * @param token + * @param password + * @param passwordConfirmation */ data class ResetPasswordRequestDto( - @ApiModelProperty(example = "test-token", required = true, value = "") - @field:JsonProperty("token", required = true) val token: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "password", - required = true - ) val password: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "passwordConfirmation", - required = true - ) val passwordConfirmation: String -) + @Schema(example = "test-token", required = true, description = "") + @get:JsonProperty("token", required = true) val token: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("password", required = true) val password: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("passwordConfirmation", required = true) val passwordConfirmation: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/TierTypeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/TierTypeDto.kt new file mode 100644 index 00000000..ef7e90f3 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/TierTypeDto.kt @@ -0,0 +1,22 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** +* +* Values: TIER_PRACTICE,TIER1,TIER2,TIER3,TIER4 +*/ +enum class TierTypeDto(val value: kotlin.String) { + + @JsonProperty("TIER_PRACTICE") TIER_PRACTICE("TIER_PRACTICE"), + @JsonProperty("TIER1") TIER1("TIER1"), + @JsonProperty("TIER2") TIER2("TIER2"), + @JsonProperty("TIER3") TIER3("TIER3"), + @JsonProperty("TIER4") TIER4("TIER4") +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/TutorialUpdateTypeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/TutorialUpdateTypeDto.kt new file mode 100644 index 00000000..6e29097b --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/TutorialUpdateTypeDto.kt @@ -0,0 +1,21 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** +* +* Values: NEXT,PREVIOUS,SKIP,RESET +*/ +enum class TutorialUpdateTypeDto(val value: kotlin.String) { + + @JsonProperty("NEXT") NEXT("NEXT"), + @JsonProperty("PREVIOUS") PREVIOUS("PREVIOUS"), + @JsonProperty("SKIP") SKIP("SKIP"), + @JsonProperty("RESET") RESET("RESET") +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UpdateCurrentUserProfileDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UpdateCurrentUserProfileDto.kt index 666e747b..9b408619 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UpdateCurrentUserProfileDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UpdateCurrentUserProfileDto.kt @@ -1,26 +1,39 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.TutorialUpdateTypeDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Update current user profile request - * @param name - * @param country - * @param college - * @param avatarId + * @param name + * @param country + * @param college + * @param avatarId + * @param updateTutorialLevel */ data class UpdateCurrentUserProfileDto( - @ApiModelProperty(example = "Test", value = "") - @field:JsonProperty("name") val name: String? = null, + @Schema(example = "Test", description = "") + @get:JsonProperty("name") val name: kotlin.String? = null, - @ApiModelProperty(example = "IN", value = "") - @field:JsonProperty("country") val country: String? = null, + @Schema(example = "IN", description = "") + @get:JsonProperty("country") val country: kotlin.String? = null, - @ApiModelProperty(example = "Test", value = "") - @field:JsonProperty("college") val college: String? = null, + @Schema(example = "Test", description = "") + @get:JsonProperty("college") val college: kotlin.String? = null, + + @Schema(example = "1", description = "") + @get:JsonProperty("avatarId") val avatarId: kotlin.Int? = null, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("updateTutorialLevel") val updateTutorialLevel: TutorialUpdateTypeDto? = null +) { + +} - @ApiModelProperty(example = "1", value = "") - @field:JsonProperty("avatarId") val avatarId: Int? = null -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestCodeRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestCodeRequestDto.kt index 045fed26..7f829a5a 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestCodeRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestCodeRequestDto.kt @@ -1,28 +1,37 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.Valid +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.CodeTypeDto +import delta.codecharacter.dtos.LanguageDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Update latest code request - * @param code - * @param language - * @param lock + * @param code + * @param language + * @param codeType + * @param lock */ data class UpdateLatestCodeRequestDto( - @ApiModelProperty( - example = "#include ", - required = true, - value = "" - ) - @field:JsonProperty("code", required = true) val code: String, + @Schema(example = "#include ", required = true, description = "") + @get:JsonProperty("code", required = true) val code: kotlin.String, @field:Valid - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty("language", required = true) val language: LanguageDto, + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("language", required = true) val language: LanguageDto, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("codeType") val codeType: CodeTypeDto? = CodeTypeDto.NORMAL, + + @Schema(example = "null", description = "") + @get:JsonProperty("lock") val lock: kotlin.Boolean? = false +) { + +} - @ApiModelProperty(example = "null", value = "") - @field:JsonProperty("lock") val lock: Boolean? = false -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestMapRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestMapRequestDto.kt index 4c5ac05d..c969bc5f 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestMapRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UpdateLatestMapRequestDto.kt @@ -1,22 +1,35 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import com.fasterxml.jackson.annotation.JsonValue +import delta.codecharacter.dtos.GameMapTypeDto +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Update latest map request - * @param map - * @param lock + * @param map + * @param mapImage + * @param mapType + * @param lock */ data class UpdateLatestMapRequestDto( - @ApiModelProperty( - example = "0000\n0010\n0100\n1000\n", - required = true, - value = "" - ) - @field:JsonProperty("map", required = true) val map: String, + @Schema(example = "0000\n0010\n0100\n1000\n", required = true, description = "") + @get:JsonProperty("map", required = true) val map: kotlin.String, + + @Schema(example = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII", required = true, description = "") + @get:JsonProperty("mapImage", required = true) val mapImage: kotlin.String, + + @field:Valid + @Schema(example = "null", description = "") + @get:JsonProperty("mapType") val mapType: GameMapTypeDto? = GameMapTypeDto.NORMAL, + + @Schema(example = "null", description = "") + @get:JsonProperty("lock") val lock: kotlin.Boolean? = false +) { + +} - @ApiModelProperty(example = "null", value = "") - @field:JsonProperty("lock") val lock: Boolean? = false -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UpdatePasswordRequestDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UpdatePasswordRequestDto.kt index 80533734..160968ad 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UpdatePasswordRequestDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UpdatePasswordRequestDto.kt @@ -1,36 +1,34 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty -import javax.validation.constraints.Pattern -import javax.validation.constraints.Size +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * Update password request - * @param oldPassword - * @param password - * @param passwordConfirmation + * @param oldPassword + * @param password + * @param passwordConfirmation */ data class UpdatePasswordRequestDto( - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "oldPassword", - required = true - ) val oldPassword: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "password", - required = true - ) val password: String, - @get:Pattern(regexp = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") - @get:Size(min = 8, max = 32) - @ApiModelProperty(example = "null", required = true, value = "") - @field:JsonProperty( - "passwordConfirmation", - required = true - ) val passwordConfirmation: String -) + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("oldPassword", required = true) val oldPassword: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("password", required = true) val password: kotlin.String, + + @get:Pattern(regexp="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,32}$") + @get:Size(min=8,max=32) + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("passwordConfirmation", required = true) val passwordConfirmation: kotlin.String +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UserStatsDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UserStatsDto.kt index 787be431..8a1bc373 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UserStatsDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UserStatsDto.kt @@ -1,29 +1,32 @@ package delta.codecharacter.dtos +import java.util.Objects import com.fasterxml.jackson.annotation.JsonProperty -import io.swagger.annotations.ApiModelProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** * User stats model - * @param rating - * @param wins - * @param losses - * @param ties + * @param rating + * @param wins + * @param losses + * @param ties */ data class UserStatsDto( - @ApiModelProperty(example = "1000", required = true, value = "") - @field:JsonProperty( - "rating", - required = true - ) val rating: java.math.BigDecimal, + @Schema(example = "1000", required = true, description = "") + @get:JsonProperty("rating", required = true) val rating: java.math.BigDecimal, - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("wins", required = true) val wins: Int = 0, + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("wins", required = true) val wins: kotlin.Int = 0, - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("losses", required = true) val losses: Int, + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("losses", required = true) val losses: kotlin.Int, + + @Schema(example = "1", required = true, description = "") + @get:JsonProperty("ties", required = true) val ties: kotlin.Int +) { + +} - @ApiModelProperty(example = "1", required = true, value = "") - @field:JsonProperty("ties", required = true) val ties: Int -) diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/VerdictDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/VerdictDto.kt index 30cca80b..9793f17d 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/VerdictDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/VerdictDto.kt @@ -1,19 +1,22 @@ package delta.codecharacter.dtos +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.* +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema /** - * Match/Game verdict - * Values: PLAYER1,PLAYER2,TIE - */ -enum class VerdictDto(val value: String) { +* Match/Game verdict +* Values: PLAYER1,PLAYER2,TIE,SUCCESS,FAILURE +*/ +enum class VerdictDto(val value: kotlin.String) { - @JsonProperty("PLAYER1") - PLAYER1("PLAYER1"), - - @JsonProperty("PLAYER2") - PLAYER2("PLAYER2"), - - @JsonProperty("TIE") - TIE("TIE"); + @JsonProperty("PLAYER1") PLAYER1("PLAYER1"), + @JsonProperty("PLAYER2") PLAYER2("PLAYER2"), + @JsonProperty("TIE") TIE("TIE"), + @JsonProperty("SUCCESS") SUCCESS("SUCCESS"), + @JsonProperty("FAILURE") FAILURE("FAILURE") } + diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 5a51b15d..4483f707 100644 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash echo "Running pre-commit checks..." diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 857b67d5..41fbc105 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "2.6.2" + id("org.springframework.boot") version "3.0.0" id("io.spring.dependency-management") version "1.0.11.RELEASE" id("org.asciidoctor.convert") version "1.5.8" jacoco @@ -29,34 +29,34 @@ repositories { val snippetsDir by extra { file("build/generated-snippets") } dependencies { - - implementation("org.springframework.boot:spring-boot-starter-amqp:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-data-mongodb:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-oauth2-client:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-security:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-validation:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-web:2.6.3") - implementation("org.springframework.boot:spring-boot-starter-websocket:2.6.3") + runtimeOnly("org.springframework.boot:spring-boot-properties-migrator") + implementation("org.springframework.boot:spring-boot-starter-amqp:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-security:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-validation:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-web:3.0.0") + implementation("org.springframework.boot:spring-boot-starter-websocket:3.0.0") implementation("io.springfox:springfox-boot-starter:3.0.0") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1") - implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.0") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.0") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.21") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21") implementation("io.jsonwebtoken:jjwt:0.9.1") implementation("javax.xml.bind:jaxb-api:2.4.0-b180830.0359") implementation("com.sendgrid:sendgrid-java:4.8.3") implementation(project(":library")) implementation("org.junit.jupiter:junit-jupiter:5.8.2") - testImplementation("com.ninja-squad:springmockk:3.1.0") - developmentOnly("org.springframework.boot:spring-boot-devtools:2.6.3") - annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:2.6.3") - testImplementation("org.springframework.boot:spring-boot-starter-test:2.6.3") { + testImplementation("com.ninja-squad:springmockk:4.0.0") + developmentOnly("org.springframework.boot:spring-boot-devtools:3.0.0") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:3.0.0") + testImplementation("org.springframework.boot:spring-boot-starter-test:3.0.0") { exclude(module = "mockito-core") } - testRuntimeOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo:3.3.1") - testImplementation("org.springframework.amqp:spring-rabbit-test:2.4.2") + testRuntimeOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring30x:4.5.2") + testImplementation("org.springframework.amqp:spring-rabbit-test:2.4.7") testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc:2.0.6.RELEASE") - testImplementation("org.springframework.security:spring-security-test:5.5.1") + testImplementation("org.springframework.security:spring-security-test:6.0.0") } allOpen { diff --git a/server/src/main/kotlin/delta/codecharacter/server/ServerApplication.kt b/server/src/main/kotlin/delta/codecharacter/server/ServerApplication.kt index 5cecc2fb..c3b518b1 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/ServerApplication.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/ServerApplication.kt @@ -4,14 +4,16 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity +import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @EnableWebSecurity -@EnableGlobalMethodSecurity(securedEnabled = true) +@EnableMethodSecurity(securedEnabled = true) @SpringBootApplication +@EnableScheduling class ServerApplication { @Value("\${cors.allowed-origin}") private lateinit var allowedOrigin: String diff --git a/server/src/main/kotlin/delta/codecharacter/server/auth/AuthService.kt b/server/src/main/kotlin/delta/codecharacter/server/auth/AuthService.kt index 50b9b0bf..ce7a822c 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/auth/AuthService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/auth/AuthService.kt @@ -43,6 +43,11 @@ class AuthService( fun forgotPassword(forgotPasswordRequestDto: ForgotPasswordRequestDto) { val email = forgotPasswordRequestDto.email + if (!forgotPasswordRequestDto.recaptchaCode.isNullOrEmpty() && + !userService.verifyReCaptcha(forgotPasswordRequestDto.recaptchaCode!!) + ) { + throw CustomException(HttpStatus.BAD_REQUEST, "Invalid Recaptcha") + } val user = userService.getUserByEmail(email).orElseThrow { throw CustomException(HttpStatus.BAD_REQUEST, "Invalid credentials") diff --git a/server/src/main/kotlin/delta/codecharacter/server/auth/jwt/JwtRequestFilter.kt b/server/src/main/kotlin/delta/codecharacter/server/auth/jwt/JwtRequestFilter.kt index 8f661f1d..aaf5ed2c 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/auth/jwt/JwtRequestFilter.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/auth/jwt/JwtRequestFilter.kt @@ -1,15 +1,15 @@ package delta.codecharacter.server.auth.jwt import delta.codecharacter.server.user.UserService +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter -import javax.servlet.FilterChain -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse @Component class JwtRequestFilter : OncePerRequestFilter() { diff --git a/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2FailureHandler.kt b/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2FailureHandler.kt index 6a08977f..6994632d 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2FailureHandler.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2FailureHandler.kt @@ -1,14 +1,14 @@ package delta.codecharacter.server.auth.oauth2 import delta.codecharacter.server.exception.CustomException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.AuthenticationException import org.springframework.security.web.authentication.AuthenticationFailureHandler import org.springframework.stereotype.Component -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse @Component class CustomOAuth2FailureHandler : AuthenticationFailureHandler { diff --git a/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2SuccessHandler.kt b/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2SuccessHandler.kt index 8c2e3ba9..7d1e84e2 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2SuccessHandler.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/auth/oauth2/CustomOAuth2SuccessHandler.kt @@ -3,6 +3,8 @@ package delta.codecharacter.server.auth.oauth2 import delta.codecharacter.server.auth.AuthService import delta.codecharacter.server.exception.CustomException import delta.codecharacter.server.user.LoginType +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -16,8 +18,6 @@ import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse @Component class CustomOAuth2SuccessHandler(@Lazy @Autowired private val authService: AuthService) : diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/Code.kt b/server/src/main/kotlin/delta/codecharacter/server/code/Code.kt new file mode 100644 index 00000000..5c3cda7e --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/code/Code.kt @@ -0,0 +1,5 @@ +package delta.codecharacter.server.code + +import java.time.Instant + +data class Code(val code: String, val language: LanguageEnum, val lastSavedAt: Instant? = null) diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/CodeController.kt b/server/src/main/kotlin/delta/codecharacter/server/code/CodeController.kt index a5c2c444..5848e99a 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/CodeController.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/CodeController.kt @@ -3,6 +3,7 @@ package delta.codecharacter.server.code import delta.codecharacter.core.CodeApi import delta.codecharacter.dtos.CodeDto import delta.codecharacter.dtos.CodeRevisionDto +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.CreateCodeRevisionRequestDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto import delta.codecharacter.server.code.code_revision.CodeRevisionService @@ -32,15 +33,15 @@ class CodeController( } @Secured(value = ["ROLE_USER"]) - override fun getCodeRevisions(): ResponseEntity> { + override fun getCodeRevisions(type: CodeTypeDto): ResponseEntity> { val user = SecurityContextHolder.getContext().authentication.principal as UserEntity - return ResponseEntity.ok(codeRevisionService.getCodeRevisions(user.id)) + return ResponseEntity.ok(codeRevisionService.getCodeRevisions(user.id, type)) } @Secured(value = ["ROLE_USER"]) - override fun getLatestCode(): ResponseEntity { + override fun getLatestCode(type: CodeTypeDto): ResponseEntity { val user = SecurityContextHolder.getContext().authentication.principal as UserEntity - return ResponseEntity.ok(latestCodeService.getLatestCode(user.id)) + return ResponseEntity.ok(latestCodeService.getLatestCode(user.id, type)) } @Secured(value = ["ROLE_USER"]) diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionEntity.kt index 7043ddaf..ddb5a7e0 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionEntity.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.code.code_revision +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.server.code.LanguageEnum import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document @@ -12,6 +13,8 @@ import java.util.UUID * * @param id * @param code + * @param codeType + * @param message * @param language * @param parentRevision * @param userId @@ -21,6 +24,7 @@ import java.util.UUID data class CodeRevisionEntity( @Id val id: UUID, val code: String, + val codeType: CodeTypeDto, val message: String, val language: LanguageEnum, @DocumentReference(lazy = true) val parentRevision: CodeRevisionEntity?, diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionRepository.kt index 2a11e5c9..0ceb16fd 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionRepository.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionRepository.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.code.code_revision +import delta.codecharacter.dtos.CodeTypeDto import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.stereotype.Repository import java.util.Optional @@ -8,6 +9,12 @@ import java.util.UUID /** Repository for [CodeRevisionEntity] */ @Repository interface CodeRevisionRepository : MongoRepository { - fun findAllByUserIdOrderByCreatedAtDesc(userId: UUID): List - fun findFirstByUserIdOrderByCreatedAtDesc(userId: UUID): Optional + fun findAllByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId: UUID, + codeType: CodeTypeDto + ): List + fun findFirstByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId: UUID, + codeType: CodeTypeDto + ): Optional } diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt index dbb9052a..3fcb5b7f 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt @@ -1,6 +1,7 @@ package delta.codecharacter.server.code.code_revision import delta.codecharacter.dtos.CodeRevisionDto +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.CreateCodeRevisionRequestDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.server.code.LanguageEnum @@ -16,11 +17,16 @@ class CodeRevisionService(@Autowired private val codeRevisionRepository: CodeRev fun createCodeRevision(userId: UUID, createCodeRevisionRequestDto: CreateCodeRevisionRequestDto) { val (code, message, language) = createCodeRevisionRequestDto val parentCodeRevision = - codeRevisionRepository.findFirstByUserIdOrderByCreatedAtDesc(userId).orElse(null) + codeRevisionRepository + .findFirstByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId, createCodeRevisionRequestDto.codeType ?: CodeTypeDto.NORMAL + ) + .orElse(null) codeRevisionRepository.save( CodeRevisionEntity( id = UUID.randomUUID(), code = code, + codeType = createCodeRevisionRequestDto.codeType ?: CodeTypeDto.NORMAL, message = message, language = LanguageEnum.valueOf(language.name), userId = userId, @@ -30,16 +36,22 @@ class CodeRevisionService(@Autowired private val codeRevisionRepository: CodeRev ) } - fun getCodeRevisions(userId: UUID): List { - return codeRevisionRepository.findAllByUserIdOrderByCreatedAtDesc(userId).map { - CodeRevisionDto( - id = it.id, - code = it.code, - message = it.message, - language = LanguageDto.valueOf(it.language.name), - createdAt = it.createdAt, - parentRevision = it.parentRevision?.id - ) - } + fun getCodeRevisions( + userId: UUID, + codeTypeDto: CodeTypeDto = CodeTypeDto.NORMAL + ): List { + return codeRevisionRepository.findAllByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId, codeTypeDto + ) + .map { + CodeRevisionDto( + id = it.id, + code = it.code, + message = it.message, + language = LanguageDto.valueOf(it.language.name), + createdAt = it.createdAt, + parentRevision = it.parentRevision?.id + ) + } } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeEntity.kt index d49eb2d7..a95e5ed5 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeEntity.kt @@ -1,23 +1,19 @@ package delta.codecharacter.server.code.latest_code -import delta.codecharacter.server.code.LanguageEnum +import delta.codecharacter.dtos.CodeTypeDto +import delta.codecharacter.server.code.Code import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document -import java.time.Instant import java.util.UUID /** * Latest code entity. * * @param userId - * @param code - * @param language - * @param lastSavedAt + * @param latestCode */ @Document(collection = "latest_code") data class LatestCodeEntity( @Id val userId: UUID, - val code: String, - val language: LanguageEnum, - val lastSavedAt: Instant, + val latestCode: HashMap, ) diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeService.kt b/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeService.kt index 85a463ab..9eaa4a0e 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeService.kt @@ -1,8 +1,10 @@ package delta.codecharacter.server.code.latest_code import delta.codecharacter.dtos.CodeDto +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto +import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.config.DefaultCodeMapConfiguration import org.springframework.beans.factory.annotation.Autowired @@ -17,34 +19,62 @@ class LatestCodeService( @Autowired private val defaultCodeMapConfiguration: DefaultCodeMapConfiguration ) { - fun getLatestCode(userId: UUID): CodeDto { - return latestCodeRepository - .findById(userId) - .orElse( - LatestCodeEntity( - userId, - code = defaultCodeMapConfiguration.defaultCode, - language = defaultCodeMapConfiguration.defaultLanguage, - lastSavedAt = Instant.MIN - ) - ) - .let { latestCode -> - CodeDto( - code = latestCode.code, - language = LanguageDto.valueOf(latestCode.language.name), - lastSavedAt = latestCode.lastSavedAt + fun getLatestCode(userId: UUID, codeType: CodeTypeDto = CodeTypeDto.NORMAL): CodeDto { + val latestCode = HashMap() + latestCode[codeType] = defaultCodeMapConfiguration.defaultLatestCode + val code: CodeDto = + latestCodeRepository + .findById(userId) + .orElse( + LatestCodeEntity( + userId = userId, + latestCode = latestCode, + ) ) - } + .let { code -> + CodeDto( + code = code.latestCode[codeType]?.code ?: defaultCodeMapConfiguration.defaultCode, + language = + LanguageDto.valueOf( + code.latestCode[codeType]?.language?.name + ?: defaultCodeMapConfiguration.defaultLanguage.name + ), + lastSavedAt = code.latestCode[codeType]?.lastSavedAt ?: Instant.MIN, + ) + } + + return code } fun updateLatestCode(userId: UUID, updateLatestCodeRequestDto: UpdateLatestCodeRequestDto) { - latestCodeRepository.save( - LatestCodeEntity( + val latestCode = HashMap() + latestCode[updateLatestCodeRequestDto.codeType ?: CodeTypeDto.NORMAL] = + Code( code = updateLatestCodeRequestDto.code, language = LanguageEnum.valueOf(updateLatestCodeRequestDto.language.name), - userId = userId, lastSavedAt = Instant.now() ) - ) + if (latestCodeRepository.findById(userId).isEmpty) { + latestCodeRepository.save( + LatestCodeEntity( + latestCode = latestCode, + userId = userId, + ) + ) + } else { + val code = latestCodeRepository.findById(userId).get() + val currentLatestCode = code.latestCode + currentLatestCode[updateLatestCodeRequestDto.codeType ?: CodeTypeDto.NORMAL] = + Code( + code = updateLatestCodeRequestDto.code, + language = LanguageEnum.valueOf(updateLatestCodeRequestDto.language.name), + lastSavedAt = Instant.now() + ) + val updateCodeEntity = + code.copy( + latestCode = currentLatestCode, + ) + latestCodeRepository.save(updateCodeEntity) + } } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeEntity.kt index 7efa092c..af3332a6 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeEntity.kt @@ -1,6 +1,7 @@ package delta.codecharacter.server.code.locked_code -import delta.codecharacter.server.code.LanguageEnum +import delta.codecharacter.dtos.CodeTypeDto +import delta.codecharacter.server.code.Code import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document import java.util.UUID @@ -9,8 +10,10 @@ import java.util.UUID * Locked code entity. * * @param userId - * @param code - * @param language + * @param lockedCode */ @Document(collection = "locked_code") -data class LockedCodeEntity(@Id val userId: UUID, val code: String, val language: LanguageEnum) +data class LockedCodeEntity( + @Id val userId: UUID, + val lockedCode: HashMap, +) diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeService.kt b/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeService.kt index d6581e6b..f02a51d2 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeService.kt @@ -1,6 +1,8 @@ package delta.codecharacter.server.code.locked_code +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto +import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.config.DefaultCodeMapConfiguration import org.springframework.beans.factory.annotation.Autowired @@ -14,26 +16,50 @@ class LockedCodeService( @Autowired private val defaultCodeMapConfiguration: DefaultCodeMapConfiguration ) { - fun getLockedCode(userId: UUID): Pair { + fun getLockedCode( + userId: UUID, + codeType: CodeTypeDto = CodeTypeDto.NORMAL + ): Pair { + val lockedCode = HashMap() + lockedCode[codeType] = defaultCodeMapConfiguration.defaultLockedCode return lockedCodeRepository .findById(userId) .orElse( LockedCodeEntity( - userId, - code = defaultCodeMapConfiguration.defaultCode, - language = defaultCodeMapConfiguration.defaultLanguage + userId = userId, + lockedCode = lockedCode, ) ) - .let { Pair(it.language, it.code) } + .let { code -> + Pair( + code.lockedCode[codeType]?.language ?: defaultCodeMapConfiguration.defaultLanguage, + code.lockedCode[codeType]?.code ?: defaultCodeMapConfiguration.defaultCode + ) + } } fun updateLockedCode(userId: UUID, updateLatestCodeRequestDto: UpdateLatestCodeRequestDto) { - lockedCodeRepository.save( - LockedCodeEntity( + val lockedCode = HashMap() + lockedCode[updateLatestCodeRequestDto.codeType ?: CodeTypeDto.NORMAL] = + Code( code = updateLatestCodeRequestDto.code, - language = LanguageEnum.valueOf(updateLatestCodeRequestDto.language.name), - userId = userId + language = LanguageEnum.valueOf(updateLatestCodeRequestDto.language.name) ) - ) + if (lockedCodeRepository.findById(userId).isEmpty) { + lockedCodeRepository.save(LockedCodeEntity(userId = userId, lockedCode = lockedCode)) + } else { + val code = lockedCodeRepository.findById(userId).get() + val currentLockCode = code.lockedCode + currentLockCode[updateLatestCodeRequestDto.codeType ?: CodeTypeDto.NORMAL] = + Code( + code = updateLatestCodeRequestDto.code, + language = LanguageEnum.valueOf(updateLatestCodeRequestDto.language.name) + ) + val updatedLockedCodeEntity = + code.copy( + lockedCode = currentLockCode, + ) + lockedCodeRepository.save(updatedLockedCodeEntity) + } } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/config/DefaultCodeMapConfiguration.kt b/server/src/main/kotlin/delta/codecharacter/server/config/DefaultCodeMapConfiguration.kt index 20e66675..00f50ea5 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/config/DefaultCodeMapConfiguration.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/config/DefaultCodeMapConfiguration.kt @@ -1,112 +1,29 @@ package delta.codecharacter.server.config +import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum +import delta.codecharacter.server.game_map.GameMap import org.springframework.context.annotation.Configuration +import java.time.Instant @Configuration data class DefaultCodeMapConfiguration( val defaultCode: String = - "#include \"player_code.h\"\n" + - "\n" + - "// This initial code is well commented and serves as a small tutorial for game\n" + - "// APIs, for more information you can refer to the documentation\n" + - "\n" + - "// This is the function player has to fill\n" + - "// You can define any new functions here that you want\n" + - "Game run(const State &state) {\n" + - "\n" + - " // Always start by instantiating a Game class object\n" + - " Game game;\n" + - "\n" + - " size_t remaining_coins = state.get_coins_left();\n" + - "\n" + - " game.logr() << \"TURN \" << state.get_turn_no() << \" LOGS:\";\n" + - "\n" + - " // Get all the attackers and defenders in the game and store it\n" + - " const std::vector &attackers = state.get_attackers();\n" + - " const std::vector &defenders = state.get_defenders();\n" + - "\n" + - " // The function get_all_valid_spawn_positions() is a helper which will give us\n" + - " // the list of valid spawn positions in map.\n" + - " // If the position we're spawning is not one of these, the player will be\n" + - " // penalized by deducting the spawn cost but not spawning the attacker\n" + - " std::vector all_valid_spawn_positions =\n" + - " get_all_valid_spawn_positions();\n" + - "\n" + - " // Lets say I want to spawn an attacker of each of the type in one turn\n" + - " // and I want to use the all_valid_spawn_positions list as well. In order to\n" + - " // keep traack of the last index in the list that we spawned at, we can use a\n" + - " // static variable in c++\n" + - "\n" + - " static int last_spawned = 0;\n" + - "\n" + - " // If there's no defenders left,we can stop spawning and save up on coins,\n" + - " // which are important for boosting game score\n" + - " if (!defenders.empty()) {\n" + - " for (size_t type_id = 1; type_id <= Constants::NO_OF_ATTACKER_TYPES;\n" + - " type_id++) {\n" + - " // Spawn the attacker of type_id at position\n" + - " // all_valid_spawn_positions[last_spawned]\n" + - "\n" + - " // There are two cases when you might be panalized\n" + - " // - Spawning at invalid position\n" + - " // - Spawning at position where you have already spawned one attacker\n" + - " // in the same turn\n" + - " //\n" + - " // We have provided helpers to check just that\n" + - "\n" + - " // game class will keep track of all your spawned positions for you and\n" + - " // provides a helper method called already_spawned_at_position(Position)\n" + - " // to check if you already spawned in the position\n" + - "\n" + - " // Mostly a good practice to check with these two helpers before spawning,\n" + - " // to save up on accidental penalties\n" + - " if (is_valid_spawn_position(all_valid_spawn_positions[last_spawned]) &&\n" + - " !game.already_spawned_at_position(\n" + - " all_valid_spawn_positions[last_spawned])) {\n" + - " // If lets say you had run out of coins left, the game will just ignore\n" + - " // the spawn\n" + - " game.spawn_attacker(type_id, all_valid_spawn_positions[last_spawned]);\n" + - "\n" + - " // This has the starting attributes for the attacker we are about to\n" + - " // spawn\n" + - " // For full information about the Attributes class refer the\n" + - " // documentation\n" + - " Attributes attackers_attributes =\n" + - " Constants::ATTACKER_TYPE_ATTRIBUTES.at(type_id);\n" + - "\n" + - " // You can use the logger we provide to show log messages in the\n" + - " // rendered game\n" + - " game.logr() << \"(\" << attackers_attributes.hp << \",\"\n" + - " << attackers_attributes.attack_power\n" + - " << \") to be spawned at Position(\"\n" + - " << all_valid_spawn_positions[last_spawned].get_x() << \",\"\n" + - " << all_valid_spawn_positions[last_spawned].get_y() << \")\"\n" + - " << '\\n';\n" + - " (last_spawned += 1) %= all_valid_spawn_positions.size();\n" + - " }\n" + - " }\n" + - " }\n" + - "\n" + - " // Now lets say you always want to set the target for the attackers[0] to\n" + - " // defenders[0]\n" + - " // To do that you do\n" + - " if (!attackers.empty() && !defenders.empty()) {\n" + - " // check if they are empty beforehand to be safe from unexpected errors\n" + - " game.set_target(attackers.front(), defenders.front());\n" + - " }\n" + - "\n" + - " // Lets log all the spawned positions for this turn\n" + - " for (auto &[type_id, pos] : game.get_spawn_positions()) {\n" + - " // you can use logger macro as well, which is an alias for game.logr()\n" + - " logger << \"Type \" << type_id << \" at Position (\" << pos.get_x() << \",\"\n" + - " << pos.get_y() << \")\\n\";\n" + - " }\n" + - "\n" + - " // always return the game object\n" + - " return game;\n" + - "}\n", + DefaultCodeMapConfiguration::class + .java + .classLoader + .getResource("player_code/cpp/run.cpp") + ?.readText() + ?: "", val defaultLanguage: LanguageEnum = LanguageEnum.CPP, val defaultMap: String = "[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]", + val defaultMapImage: String = + "iVBORw0KGgoAAAANSUhEUgAAAnsAAAGJCAYAAADyhvUYAAEAAElEQVR4Xux9B7yeRbH+nH5Oeu+dNEgCCR2lCCpWVCxgRbFd21Wv93qtf/Xa+7323rAginptF0Ep0ktoIUACIb3XU3J6+88z38xm3v3e75yTAJrk7PcjnK/suzs7Ozvz7OzsLFF6JQ4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQOJA4kDiQO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AoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAk9PBP5/E5Y8EhJZfuQAAAAASUVORK5CYII=", + val defaultLatestCode: Code = + Code(code = defaultCode, language = defaultLanguage, lastSavedAt = Instant.MIN), + val defaultLockedCode: Code = Code(code = defaultCode, language = defaultLanguage), + val defaultLatestGameMap: GameMap = + GameMap(mapImage = defaultMapImage, map = defaultMap, lastSavedAt = Instant.MIN), + val defaultLockedGameMap: GameMap = GameMap(mapImage = defaultMapImage, map = defaultMap) ) diff --git a/server/src/main/kotlin/delta/codecharacter/server/config/GameConfiguration.kt b/server/src/main/kotlin/delta/codecharacter/server/config/GameConfiguration.kt index 4427c107..7b2a00e9 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/config/GameConfiguration.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/config/GameConfiguration.kt @@ -14,16 +14,42 @@ class GameConfiguration { return GameParameters( attackers = setOf( - Attacker(id = 1, hp = 10, range = 3, attackPower = 3, speed = 3, price = 1), - Attacker(id = 2, hp = 10, range = 3, attackPower = 3, speed = 3, price = 1), + Attacker( + id = 1, + hp = 10, + range = 2, + attackPower = 4, + speed = 4, + price = 2, + aerial = 0, + ), + Attacker( + id = 2, + hp = 20, + range = 4, + attackPower = 2, + speed = 2, + price = 2, + aerial = 0, + ), + Attacker( + id = 3, + hp = 15, + range = 4, + attackPower = 2, + speed = 4, + price = 4, + aerial = 1, + ), ), defenders = setOf( - Defender(id = 1, hp = 10, range = 4, attackPower = 5, price = 1), - Defender(id = 2, hp = 10, range = 6, attackPower = 5, price = 1), + Defender(id = 1, hp = 400, range = 4, attackPower = 10, price = 25, aerial = 0), + Defender(id = 2, hp = 600, range = 6, attackPower = 20, price = 50, aerial = 0), + Defender(id = 3, hp = 400, range = 6, attackPower = 15, price = 100, aerial = 1), ), numberOfTurns = 500, - numberOfCoins = 1000, + numberOfCoins = 1800, ) } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/config/LogicConfiguration.kt b/server/src/main/kotlin/delta/codecharacter/server/config/LogicConfiguration.kt index 3dddaa9a..39c6abbb 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/config/LogicConfiguration.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/config/LogicConfiguration.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.config +import delta.codecharacter.server.logic.daily_challenge_score.DailyChallengeScoreAlgorithm import delta.codecharacter.server.logic.rating.GlickoRatingAlgorithm import delta.codecharacter.server.logic.rating.RatingAlgorithm import delta.codecharacter.server.logic.validation.MapValidator @@ -25,4 +26,9 @@ class LogicConfiguration { fun mapValidator(): MapValidator { return MapValidator() } + + @Bean + fun dailyChallengeScoreAlgorithm(): DailyChallengeScoreAlgorithm { + return DailyChallengeScoreAlgorithm(gameConfiguration = GameConfiguration()) + } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/config/WebSecurityConfiguration.kt b/server/src/main/kotlin/delta/codecharacter/server/config/WebSecurityConfiguration.kt index eb4aa51e..d769160c 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/config/WebSecurityConfiguration.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/config/WebSecurityConfiguration.kt @@ -12,18 +12,19 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.config.web.servlet.invoke import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler import org.springframework.security.web.firewall.RequestRejectedHandler @Configuration -class WebSecurityConfiguration : WebSecurityConfigurerAdapter() { +class WebSecurityConfiguration { @Autowired private lateinit var jwtRequestFilter: JwtRequestFilter @Autowired private lateinit var userService: UserService @@ -34,33 +35,44 @@ class WebSecurityConfiguration : WebSecurityConfigurerAdapter() { @Value("\${cors.enabled}") private val corsEnabled: Boolean = false - override fun configure(http: HttpSecurity?) { - http { - csrf { disable() } - oauth2Login { - userInfoEndpoint { - oidcUserService = customOidcUserService - userService = customOAuth2UserService + @Bean + fun filterChain(http: HttpSecurity?): SecurityFilterChain? { + + if (http != null) { + http.invoke { + csrf { disable() } + oauth2Login { + userInfoEndpoint { + oidcUserService = customOidcUserService + userService = customOAuth2UserService + } + authenticationSuccessHandler = customOAuth2SuccessHandler + authenticationFailureHandler = customOAuth2FailureHandler } - authenticationSuccessHandler = customOAuth2SuccessHandler - authenticationFailureHandler = customOAuth2FailureHandler + authorizeRequests { authorize(HttpMethod.OPTIONS, "/**", permitAll) } + cors { if (!corsEnabled) disable() } + sessionManagement { sessionCreationPolicy = SessionCreationPolicy.STATELESS } + addFilterBefore(jwtRequestFilter) } - authorizeRequests { authorize(HttpMethod.OPTIONS, "/**", permitAll) } - cors { if (!corsEnabled) disable() } - sessionManagement { sessionCreationPolicy = SessionCreationPolicy.STATELESS } - addFilterBefore(jwtRequestFilter) + return http.build() } + return null } - - override fun configure(auth: AuthenticationManagerBuilder?) { - auth?.userDetailsService(userService)?.passwordEncoder(passwordEncoder()) + @Bean + fun authenticationProvider(): DaoAuthenticationProvider { + val daoAuthenticationProvider = + DaoAuthenticationProvider().also { + it.setUserDetailsService(userService) + it.setPasswordEncoder(passwordEncoder()) + } + return daoAuthenticationProvider } - @Bean - override fun authenticationManagerBean(): AuthenticationManager { - return super.authenticationManagerBean() + fun authenticationManager( + authenticationConfiguration: AuthenticationConfiguration + ): AuthenticationManager { + return authenticationConfiguration.authenticationManager } - @Bean fun passwordEncoder() = BCryptPasswordEncoder() @Bean diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeController.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeController.kt new file mode 100644 index 00000000..1dc1b954 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeController.kt @@ -0,0 +1,41 @@ +package delta.codecharacter.server.daily_challenge + +import delta.codecharacter.core.DailyChallengesApi +import delta.codecharacter.dtos.DailyChallengeGetRequestDto +import delta.codecharacter.dtos.DailyChallengeLeaderBoardResponseDto +import delta.codecharacter.dtos.DailyChallengeMatchRequestDto +import delta.codecharacter.server.match.MatchService +import delta.codecharacter.server.user.UserEntity +import delta.codecharacter.server.user.public_user.PublicUserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.ResponseEntity +import org.springframework.security.access.annotation.Secured +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.RestController + +@RestController +class DailyChallengeController( + @Autowired private val dailyChallengeService: DailyChallengeService, + @Autowired private val matchService: MatchService, + @Autowired private val publicUserService: PublicUserService +) : DailyChallengesApi { + @Secured(value = ["ROLE_USER"]) + override fun getDailyChallenge(): ResponseEntity { + val user = SecurityContextHolder.getContext().authentication.principal as UserEntity + return ResponseEntity.ok(dailyChallengeService.getDailyChallengeByDateForUser(user.id)) + } + override fun getDailyChallengeLeaderBoard( + page: Int?, + size: Int? + ): ResponseEntity> { + return ResponseEntity.ok(publicUserService.getDailyChallengeLeaderboard(page, size)) + } + + @Secured(value = ["ROLE_USER"]) + override fun createDailyChallengeMatch( + dailyChallengeMatchRequestDto: DailyChallengeMatchRequestDto + ): ResponseEntity { + val user = SecurityContextHolder.getContext().authentication.principal as UserEntity + return ResponseEntity.ok(matchService.createDCMatch(user.id, dailyChallengeMatchRequestDto)) + } +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeEntity.kt new file mode 100644 index 00000000..fde980eb --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeEntity.kt @@ -0,0 +1,21 @@ +package delta.codecharacter.server.daily_challenge + +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.dtos.DailyChallengeObjectDto +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import java.util.UUID + +@Document(collection = "daily_challenges") +data class DailyChallengeEntity( + @Id val id: UUID, + val day: Int, + val challName: String, + val challType: ChallengeTypeDto, + val chall: DailyChallengeObjectDto, + val description: String?, + val perfectScore: Int, + val numberOfCompletions: Int, + val toleratedDestruction: Int, + val map: String +) diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeRepository.kt new file mode 100644 index 00000000..8e0417a5 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeRepository.kt @@ -0,0 +1,11 @@ +package delta.codecharacter.server.daily_challenge + +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository +import java.util.Optional +import java.util.UUID + +@Repository +interface DailyChallengeRepository : MongoRepository { + fun findByDay(day: Int): Optional +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeService.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeService.kt new file mode 100644 index 00000000..01aa9438 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeService.kt @@ -0,0 +1,85 @@ +package delta.codecharacter.server.daily_challenge + +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.dtos.DailyChallengeGetRequestDto +import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchVerdictEnum +import delta.codecharacter.server.exception.CustomException +import delta.codecharacter.server.game.GameEntity +import delta.codecharacter.server.game.GameStatusEnum +import delta.codecharacter.server.logic.daily_challenge_score.DailyChallengeScoreAlgorithm +import delta.codecharacter.server.user.public_user.PublicUserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.time.Duration +import java.time.Instant +import java.util.UUID + +@Service +class DailyChallengeService( + @Autowired private val dailyChallengeRepository: DailyChallengeRepository, + @Autowired private val publicUserService: PublicUserService, + @Autowired private val dailyChallengeScoreAlgorithm: DailyChallengeScoreAlgorithm +) { + + @Value("\${environment.event-start-date}") private lateinit var startDate: String + + fun findNumberOfDays(): Int { + val givenDateTime = Instant.parse(startDate) + val nowDateTime = Instant.now() + val period: Duration = Duration.between(givenDateTime, nowDateTime) + return period.toDays().toInt() + } + + fun getDailyChallengeByDate(): DailyChallengeEntity { + val currentDailyChallenge = + dailyChallengeRepository.findByDay(findNumberOfDays()).orElseThrow { + throw CustomException(HttpStatus.BAD_REQUEST, "Invalid Request") + } + return currentDailyChallenge + } + + fun getDailyChallengeByDateForUser(userId: UUID): DailyChallengeGetRequestDto { + val user = publicUserService.getPublicUser(userId) + val currentDailyChallenge = getDailyChallengeByDate() + return DailyChallengeGetRequestDto( + challName = currentDailyChallenge.challName, + chall = currentDailyChallenge.chall, + challType = currentDailyChallenge.challType, + description = currentDailyChallenge.description, + completionStatus = user.dailyChallengeHistory.containsKey(currentDailyChallenge.day) + ) + } + + fun completeDailyChallenge(gameEntity: GameEntity, userId: UUID): DailyChallengeMatchVerdictEnum { + val (_, coinsUsed, destruction, _, _) = gameEntity + if (gameEntity.status == GameStatusEnum.EXECUTE_ERROR) + return DailyChallengeMatchVerdictEnum.FAILURE + val currentDailyChallenge = getDailyChallengeByDate() + if (( + currentDailyChallenge.challType == ChallengeTypeDto.MAP && + destruction > currentDailyChallenge.toleratedDestruction + ) || + ( + currentDailyChallenge.challType == ChallengeTypeDto.CODE && + destruction < currentDailyChallenge.toleratedDestruction + ) + ) { + val score = + dailyChallengeScoreAlgorithm.getDailyChallengeScore( + playerCoinsUsed = coinsUsed, + playerDestruction = destruction, + dailyChallenge = currentDailyChallenge, + ) + val updatedDailyChallenge = + currentDailyChallenge.copy( + numberOfCompletions = currentDailyChallenge.numberOfCompletions + 1 + ) + dailyChallengeRepository.save(updatedDailyChallenge) + publicUserService.updateDailyChallengeScore(userId, score, currentDailyChallenge) + return DailyChallengeMatchVerdictEnum.SUCCESS + } + return DailyChallengeMatchVerdictEnum.FAILURE + } +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchEntity.kt new file mode 100644 index 00000000..8365db42 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchEntity.kt @@ -0,0 +1,18 @@ +package delta.codecharacter.server.daily_challenge.match + +import delta.codecharacter.server.game.GameEntity +import delta.codecharacter.server.user.public_user.PublicUserEntity +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import org.springframework.data.mongodb.core.mapping.DocumentReference +import java.time.Instant +import java.util.UUID + +@Document(collection = "daily_challenge_match") +data class DailyChallengeMatchEntity( + @Id val id: UUID, + @DocumentReference(lazy = true) val game: GameEntity, + val verdict: DailyChallengeMatchVerdictEnum, + @DocumentReference(lazy = true) val user: PublicUserEntity, + val createdAt: Instant +) diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchRepository.kt new file mode 100644 index 00000000..78077a8c --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchRepository.kt @@ -0,0 +1,11 @@ +package delta.codecharacter.server.daily_challenge.match + +import delta.codecharacter.server.user.public_user.PublicUserEntity +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface DailyChallengeMatchRepository : MongoRepository { + fun findByUserOrderByCreatedAtDesc(user: PublicUserEntity): List +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchVerdictEnum.kt b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchVerdictEnum.kt new file mode 100644 index 00000000..b28f0df1 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/daily_challenge/match/DailyChallengeMatchVerdictEnum.kt @@ -0,0 +1,7 @@ +package delta.codecharacter.server.daily_challenge.match + +enum class DailyChallengeMatchVerdictEnum { + STARTED, + SUCCESS, + FAILURE +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/exception/RestExceptionHandler.kt b/server/src/main/kotlin/delta/codecharacter/server/exception/RestExceptionHandler.kt index 9ae3b973..4a2066ac 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/exception/RestExceptionHandler.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/exception/RestExceptionHandler.kt @@ -1,10 +1,13 @@ package delta.codecharacter.server.exception +import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import jakarta.validation.ConstraintViolationException import org.springframework.core.Ordered import org.springframework.core.annotation.Order import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatusCode import org.springframework.http.ResponseEntity import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.web.bind.MethodArgumentNotValidException @@ -12,7 +15,6 @@ import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.context.request.WebRequest import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler -import javax.validation.ConstraintViolationException @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice @@ -21,24 +23,27 @@ class RestExceptionHandler : ResponseEntityExceptionHandler() { override fun handleHttpMessageNotReadable( ex: HttpMessageNotReadableException, headers: HttpHeaders, - status: HttpStatus, + status: HttpStatusCode, request: WebRequest - ): ResponseEntity { - val cause = ex.cause - return if (cause is MissingKotlinParameterException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(mapOf("message" to "${cause.parameter.name} is missing")) - } else { - ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("message" to "Unknown error")) + ): ResponseEntity? { + return when (val cause = ex.cause) { + is MissingKotlinParameterException -> + ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("message" to "${cause.parameter.name} is missing")) + is InvalidFormatException -> + ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(mapOf("message" to "${cause.value} is of Invalid Format")) + else -> + ResponseEntity.status(HttpStatus.BAD_REQUEST).body(mapOf("message" to "Unknown Error")) } } override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, - status: HttpStatus, + status: HttpStatusCode, request: WebRequest - ): ResponseEntity { + ): ResponseEntity? { return if (ex.bindingResult.fieldErrors.isNotEmpty()) { val fields = mutableListOf() ex.bindingResult.fieldErrors.forEach { fieldError -> fields.add(fieldError.field) } diff --git a/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt b/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt index 3c2fc7a2..d091ae93 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt @@ -56,7 +56,6 @@ class GameService( fun updateGameStatus(gameStatusUpdateJson: String): GameEntity { val gameStatusUpdateEntity = mapper.readValue(gameStatusUpdateJson, GameStatusUpdateEntity::class.java) - val oldGameEntity = gameRepository.findById(gameStatusUpdateEntity.gameId).orElseThrow { throw CustomException(HttpStatus.NOT_FOUND, "Game not found") diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMap.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMap.kt new file mode 100644 index 00000000..b19e6b30 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMap.kt @@ -0,0 +1,5 @@ +package delta.codecharacter.server.game_map + +import java.time.Instant + +data class GameMap(val mapImage: String, val map: String, val lastSavedAt: Instant? = null) diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMapController.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMapController.kt index 3661fcc8..86e76a87 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMapController.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/GameMapController.kt @@ -4,6 +4,8 @@ import delta.codecharacter.core.MapApi import delta.codecharacter.dtos.CreateMapRevisionRequestDto import delta.codecharacter.dtos.GameMapDto import delta.codecharacter.dtos.GameMapRevisionDto +import delta.codecharacter.dtos.GameMapTypeDto +import delta.codecharacter.dtos.MapCommitByCommitIdResponseDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto import delta.codecharacter.server.game_map.latest_map.LatestMapService import delta.codecharacter.server.game_map.locked_map.LockedMapService @@ -14,6 +16,7 @@ import org.springframework.http.ResponseEntity import org.springframework.security.access.annotation.Secured import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.RestController +import java.util.UUID @RestController class GameMapController( @@ -32,17 +35,16 @@ class GameMapController( } @Secured(value = ["ROLE_USER"]) - override fun getMapRevisions(): ResponseEntity> { + override fun getLatestMap(type: GameMapTypeDto): ResponseEntity { val user = SecurityContextHolder.getContext().authentication.principal as UserEntity - return ResponseEntity.ok(mapRevisionService.getMapRevisions(user.id)) + return ResponseEntity.ok(latestMapService.getLatestMap(user.id, type)) } @Secured(value = ["ROLE_USER"]) - override fun getLatestMap(): ResponseEntity { + override fun getMapRevisions(type: GameMapTypeDto): ResponseEntity> { val user = SecurityContextHolder.getContext().authentication.principal as UserEntity - return ResponseEntity.ok(latestMapService.getLatestMap(user.id)) + return ResponseEntity.ok(mapRevisionService.getMapRevisions(user.id, type)) } - @Secured(value = ["ROLE_USER"]) override fun updateLatestMap( updateLatestMapRequestDto: UpdateLatestMapRequestDto @@ -54,4 +56,12 @@ class GameMapController( } return ResponseEntity.ok().build() } + + @Secured(value = ["ROLE_USER"]) + override fun getMapByCommitID(commitId: UUID): ResponseEntity { + val user = SecurityContextHolder.getContext().authentication.principal as UserEntity + return ResponseEntity.ok( + mapRevisionService.getMapRevisionByCommitId(user.id, commitId = commitId) + ) + } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapEntity.kt index d566a7ab..492fd4a4 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapEntity.kt @@ -1,20 +1,19 @@ package delta.codecharacter.server.game_map.latest_map +import delta.codecharacter.dtos.GameMapTypeDto +import delta.codecharacter.server.game_map.GameMap import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document -import java.time.Instant import java.util.UUID /** * Latest map entity. * * @param userId - * @param map - * @param lastSavedAt + * @param latestMap */ @Document(collection = "latest_map") data class LatestMapEntity( @Id val userId: UUID, - val map: String, - val lastSavedAt: Instant, + val latestMap: HashMap, ) diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapService.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapService.kt index 39db1143..7765ed17 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapService.kt @@ -1,8 +1,10 @@ package delta.codecharacter.server.game_map.latest_map import delta.codecharacter.dtos.GameMapDto +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto import delta.codecharacter.server.config.DefaultCodeMapConfiguration +import delta.codecharacter.server.game_map.GameMap import delta.codecharacter.server.logic.validation.MapValidator import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -17,23 +19,53 @@ class LatestMapService( @Autowired private val mapValidator: MapValidator, ) { - fun getLatestMap(userId: UUID): GameMapDto { + fun getLatestMap(userId: UUID, mapType: GameMapTypeDto = GameMapTypeDto.NORMAL): GameMapDto { + val defaultMap = HashMap() + defaultMap[mapType] = defaultCodeMapConfiguration.defaultLatestGameMap return latestMapRepository .findById(userId) .orElse( LatestMapEntity( - userId, - map = defaultCodeMapConfiguration.defaultMap, - lastSavedAt = Instant.MIN, + userId = userId, + latestMap = defaultMap, ) ) - .let { latestMap -> GameMapDto(map = latestMap.map, lastSavedAt = latestMap.lastSavedAt) } + .let { latestMap -> + GameMapDto( + map = latestMap.latestMap[mapType]?.map ?: defaultCodeMapConfiguration.defaultMap, + mapImage = latestMap.latestMap[mapType]?.mapImage + ?: defaultCodeMapConfiguration.defaultMapImage, + lastSavedAt = latestMap.latestMap[mapType]?.lastSavedAt ?: Instant.MIN + ) + } } fun updateLatestMap(userId: UUID, updateLatestMapDto: UpdateLatestMapRequestDto) { mapValidator.validateMap(updateLatestMapDto.map) - latestMapRepository.save( - LatestMapEntity(map = updateLatestMapDto.map, userId = userId, lastSavedAt = Instant.now()) - ) + val latestMap = HashMap() + latestMap[updateLatestMapDto.mapType ?: GameMapTypeDto.NORMAL] = + GameMap( + mapImage = updateLatestMapDto.mapImage, + map = updateLatestMapDto.map, + lastSavedAt = Instant.now() + ) + if (latestMapRepository.findById(userId).isEmpty) { + latestMapRepository.save( + LatestMapEntity( + userId = userId, + latestMap = latestMap, + ) + ) + } else { + val map = latestMapRepository.findById(userId).get() + map.latestMap[updateLatestMapDto.mapType ?: GameMapTypeDto.NORMAL] = + GameMap( + mapImage = updateLatestMapDto.mapImage, + map = updateLatestMapDto.map, + lastSavedAt = Instant.now() + ) + val updatedMap = map.copy(latestMap = map.latestMap) + latestMapRepository.save(updatedMap) + } } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapEntity.kt index 30f44ad9..4e0c42cd 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapEntity.kt @@ -1,5 +1,7 @@ package delta.codecharacter.server.game_map.locked_map +import delta.codecharacter.dtos.GameMapTypeDto +import delta.codecharacter.server.game_map.GameMap import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document import java.util.UUID @@ -8,7 +10,7 @@ import java.util.UUID * Locked map entity. * * @param userId - * @param map + * @param lockedMap */ @Document(collection = "locked_map") -data class LockedMapEntity(@Id val userId: UUID, val map: String) +data class LockedMapEntity(@Id val userId: UUID, val lockedMap: HashMap) diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapService.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapService.kt index bea9c999..a30cebf2 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapService.kt @@ -1,7 +1,9 @@ package delta.codecharacter.server.game_map.locked_map +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto import delta.codecharacter.server.config.DefaultCodeMapConfiguration +import delta.codecharacter.server.game_map.GameMap import delta.codecharacter.server.logic.validation.MapValidator import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -15,15 +17,37 @@ class LockedMapService( @Autowired private val mapValidator: MapValidator, ) { - fun getLockedMap(userId: UUID): String { + fun getLockedMap(userId: UUID, mapType: GameMapTypeDto? = GameMapTypeDto.NORMAL): String { + val defaultMap = HashMap() + defaultMap[mapType ?: GameMapTypeDto.NORMAL] = defaultCodeMapConfiguration.defaultLockedGameMap return lockedMapRepository .findById(userId) - .orElse(LockedMapEntity(userId, defaultCodeMapConfiguration.defaultMap)) - .map + .orElse(LockedMapEntity(userId = userId, lockedMap = defaultMap)) + .lockedMap[mapType] + ?.map + ?: defaultCodeMapConfiguration.defaultMap } fun updateLockedMap(userId: UUID, updateLatestMapRequestDto: UpdateLatestMapRequestDto) { mapValidator.validateMap(updateLatestMapRequestDto.map) - lockedMapRepository.save(LockedMapEntity(map = updateLatestMapRequestDto.map, userId = userId)) + val lockedMap = HashMap() + lockedMap[updateLatestMapRequestDto.mapType ?: GameMapTypeDto.NORMAL] = + GameMap(mapImage = updateLatestMapRequestDto.mapImage, map = updateLatestMapRequestDto.map) + if (lockedMapRepository.findById(userId).isEmpty) { + lockedMapRepository.save( + LockedMapEntity( + userId = userId, + lockedMap = lockedMap, + ) + ) + } else { + val map = lockedMapRepository.findById(userId).get() + map.lockedMap[updateLatestMapRequestDto.mapType ?: GameMapTypeDto.NORMAL] = + GameMap( + mapImage = updateLatestMapRequestDto.mapImage, map = updateLatestMapRequestDto.map + ) + val updatedMap = map.copy(lockedMap = map.lockedMap) + lockedMapRepository.save(updatedMap) + } } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionEntity.kt index 88200dcf..8018c868 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionEntity.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.game_map.map_revision +import delta.codecharacter.dtos.GameMapTypeDto import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.DocumentReference @@ -11,6 +12,9 @@ import java.util.UUID * * @param id * @param map + * @param mapImage + * @param mapType + * @param message * @param parentRevision * @param userId * @param createdAt @@ -19,6 +23,8 @@ import java.util.UUID data class MapRevisionEntity( @Id val id: UUID, val map: String, + val mapImage: String, + val mapType: GameMapTypeDto, val message: String, @DocumentReference(lazy = true) val parentRevision: MapRevisionEntity?, val userId: UUID, diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionRepository.kt index 66b1f6c7..5046c792 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionRepository.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionRepository.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.game_map.map_revision +import delta.codecharacter.dtos.GameMapTypeDto import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.stereotype.Repository import java.util.Optional @@ -8,6 +9,14 @@ import java.util.UUID /** Repository for [MapRevisionEntity] */ @Repository interface MapRevisionRepository : MongoRepository { - fun findAllByUserIdOrderByCreatedAtDesc(userId: UUID): List - fun findFirstByUserIdOrderByCreatedAtDesc(userId: UUID): Optional + fun findAllByUserIdAndMapTypeOrderByCreatedAtDesc( + userId: UUID, + mapType: GameMapTypeDto + ): List + fun findFirstByUserIdAndMapTypeOrderByCreatedAtDesc( + userId: UUID, + mapType: GameMapTypeDto + ): Optional + + fun findByUserIdAndId(userId: UUID, commitId: UUID): Optional } diff --git a/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionService.kt b/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionService.kt index 3def48a6..3cdb8747 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionService.kt @@ -2,8 +2,12 @@ package delta.codecharacter.server.game_map.map_revision import delta.codecharacter.dtos.CreateMapRevisionRequestDto import delta.codecharacter.dtos.GameMapRevisionDto +import delta.codecharacter.dtos.GameMapTypeDto +import delta.codecharacter.dtos.MapCommitByCommitIdResponseDto +import delta.codecharacter.server.exception.CustomException import delta.codecharacter.server.logic.validation.MapValidator import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import java.time.Instant import java.util.UUID @@ -16,14 +20,20 @@ class MapRevisionService( ) { fun createMapRevision(userId: UUID, createMapRevisionRequestDto: CreateMapRevisionRequestDto) { - val (map, message) = createMapRevisionRequestDto + val (map, _, message, mapType) = createMapRevisionRequestDto mapValidator.validateMap(map) val parentCodeRevision = - mapRevisionRepository.findFirstByUserIdOrderByCreatedAtDesc(userId).orElse(null) + mapRevisionRepository + .findFirstByUserIdAndMapTypeOrderByCreatedAtDesc( + userId, mapType ?: GameMapTypeDto.NORMAL + ) + .orElse(null) mapRevisionRepository.save( MapRevisionEntity( id = UUID.randomUUID(), map = map, + mapType = mapType ?: GameMapTypeDto.NORMAL, + mapImage = createMapRevisionRequestDto.mapImage, message = message, userId = userId, parentRevision = parentCodeRevision, @@ -32,15 +42,34 @@ class MapRevisionService( ) } - fun getMapRevisions(userId: UUID): List { - return mapRevisionRepository.findAllByUserIdOrderByCreatedAtDesc(userId).map { - GameMapRevisionDto( - id = it.id, - map = it.map, - message = it.message, - parentRevision = it.parentRevision?.id, - createdAt = it.createdAt - ) - } + fun getMapRevisions( + userId: UUID, + mapType: GameMapTypeDto? = GameMapTypeDto.NORMAL + ): List { + return mapRevisionRepository.findAllByUserIdAndMapTypeOrderByCreatedAtDesc( + userId, mapType ?: GameMapTypeDto.NORMAL + ) + .map { + GameMapRevisionDto( + id = it.id, + map = it.map, + message = it.message, + parentRevision = it.parentRevision?.id, + createdAt = it.createdAt + ) + } + } + + fun getMapRevisionByCommitId(userId: UUID, commitId: UUID): MapCommitByCommitIdResponseDto { + val map: MapCommitByCommitIdResponseDto = + mapRevisionRepository + .findByUserIdAndId(userId, commitId) + .orElseThrow { throw CustomException(HttpStatus.BAD_REQUEST, "User not found") } + .let { mapRevisionEntity -> + MapCommitByCommitIdResponseDto( + map = mapRevisionEntity.map, mapImage = mapRevisionEntity.mapImage + ) + } + return map } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt b/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt index 41db10f6..1c64cc11 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt @@ -2,6 +2,7 @@ package delta.codecharacter.server.leaderboard import delta.codecharacter.core.LeaderboardApi import delta.codecharacter.dtos.LeaderboardEntryDto +import delta.codecharacter.dtos.TierTypeDto import delta.codecharacter.server.user.public_user.PublicUserService import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.ResponseEntity @@ -10,7 +11,11 @@ import org.springframework.web.bind.annotation.RestController @RestController class LeaderboardController(@Autowired private val publicUserService: PublicUserService) : LeaderboardApi { - override fun getLeaderboard(page: Int?, size: Int?): ResponseEntity> { - return ResponseEntity.ok(publicUserService.getLeaderboard(page, size)) + override fun getLeaderboard( + page: Int?, + size: Int?, + tier: TierTypeDto? + ): ResponseEntity> { + return ResponseEntity.ok(publicUserService.getLeaderboard(page, size, tier)) } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/logic/daily_challenge_score/DailyChallengeScoreAlgorithm.kt b/server/src/main/kotlin/delta/codecharacter/server/logic/daily_challenge_score/DailyChallengeScoreAlgorithm.kt new file mode 100644 index 00000000..3c0c896a --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/logic/daily_challenge_score/DailyChallengeScoreAlgorithm.kt @@ -0,0 +1,70 @@ +package delta.codecharacter.server.logic.daily_challenge_score + +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.server.config.GameConfiguration +import delta.codecharacter.server.daily_challenge.DailyChallengeEntity +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import java.time.Duration +import java.time.Instant +import kotlin.math.exp + +class DailyChallengeScoreAlgorithm(@Autowired private val gameConfiguration: GameConfiguration) : + ScoreAlgorithm { + + @Value("\${environment.event-start-date}") private lateinit var startDate: String + private val perfectBasePartConstant = 0.7 + private val perfectTimePartConstant = 0.3 + private val exponentialConstantForBasePart = 150 + private val exponentialConstantForTimePart = 15 + private val secondsInADay = 86400 + private val secondsInAnHour = 3600 + + override fun getHoursSinceDailyChallengeLaunched(): Double { + val givenDateTime = Instant.parse(startDate) + val nowDateTime = Instant.now() + val period: Duration = Duration.between(givenDateTime, nowDateTime) + + return (period.toSeconds().toDouble().rem(secondsInADay)) / secondsInAnHour + } + + override fun getPlayerBaseScore( + coinsLeftPercent: Double, + destructionPercent: Double, + perfectBaseScore: Double, + challType: ChallengeTypeDto + ): Double { + if (challType == ChallengeTypeDto.CODE) + return ((100.0 - coinsLeftPercent) + (2 * (100 - destructionPercent)) + perfectBaseScore) + return (coinsLeftPercent + (2 * destructionPercent) + perfectBaseScore) + } + + override fun getPlayerTimeScore(perfectTimeScore: Double): Double { + val hours = getHoursSinceDailyChallengeLaunched() + return perfectTimeScore * exp((-1) * (hours / exponentialConstantForTimePart)) + } + + override fun getDailyChallengeScore( + playerCoinsUsed: Int, + playerDestruction: Double, + dailyChallenge: DailyChallengeEntity + ): Double { + val totalCoins = gameConfiguration.gameParameters().numberOfCoins + val (_, _, _, challType, _, _, perfectScore, numberOfCompletions) = dailyChallenge + val perfectBasePart = + perfectBasePartConstant * + perfectScore * + exp(((-1) * (numberOfCompletions.toDouble() / exponentialConstantForBasePart))) + val perfectTimePart = perfectTimePartConstant * perfectScore + val coinsLeftPercentage = ((totalCoins - playerCoinsUsed.toDouble()) / totalCoins) * 100 + return ( + ( + getPlayerBaseScore( + coinsLeftPercentage, playerDestruction, perfectBasePart, challType + ) + + getPlayerTimeScore(perfectTimePart) + ) * 100.0 + ) + .toInt() / 100.0 + } +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/logic/daily_challenge_score/ScoreAlgorithm.kt b/server/src/main/kotlin/delta/codecharacter/server/logic/daily_challenge_score/ScoreAlgorithm.kt new file mode 100644 index 00000000..62c12c5f --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/logic/daily_challenge_score/ScoreAlgorithm.kt @@ -0,0 +1,25 @@ +package delta.codecharacter.server.logic.daily_challenge_score + +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.server.daily_challenge.DailyChallengeEntity + +interface ScoreAlgorithm { + fun getDailyChallengeScore( + playerCoinsUsed: Int, + playerDestruction: Double, + dailyChallenge: DailyChallengeEntity + ): Double + + fun getPlayerBaseScore( + coinsLeftPercent: Double, + destructionPercent: Double, + perfectBaseScore: Double, + challType: ChallengeTypeDto + ): Double + + fun getHoursSinceDailyChallengeLaunched(): Double + + fun getPlayerTimeScore( + perfectTimeScore: Double, + ): Double +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchEntity.kt new file mode 100644 index 00000000..9cfd4e4c --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchEntity.kt @@ -0,0 +1,8 @@ +package delta.codecharacter.server.match + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import java.util.UUID + +@Document(collection = "auto_match") +data class AutoMatchEntity(@Id val matchId: UUID, val tries: Int) diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchRepository.kt new file mode 100644 index 00000000..6637962f --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchRepository.kt @@ -0,0 +1,7 @@ +package delta.codecharacter.server.match + +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository interface AutoMatchRepository : MongoRepository diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt index 124ba5b8..a5d94a55 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt @@ -9,4 +9,5 @@ import java.util.UUID interface MatchRepository : MongoRepository { fun findTop10ByOrderByTotalPointsDesc(): List fun findByPlayer1OrderByCreatedAtDesc(player1: PublicUserEntity): List + fun findByIdIn(matchIds: List): List } diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt b/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt index 4690b3ae..d7d63e8b 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt @@ -1,27 +1,37 @@ package delta.codecharacter.server.match import com.fasterxml.jackson.databind.ObjectMapper +import delta.codecharacter.dtos.ChallengeTypeDto import delta.codecharacter.dtos.CreateMatchRequestDto +import delta.codecharacter.dtos.DailyChallengeMatchRequestDto import delta.codecharacter.dtos.GameDto import delta.codecharacter.dtos.GameStatusDto import delta.codecharacter.dtos.MatchDto import delta.codecharacter.dtos.MatchModeDto import delta.codecharacter.dtos.PublicUserDto +import delta.codecharacter.dtos.TierTypeDto import delta.codecharacter.dtos.VerdictDto import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.code.code_revision.CodeRevisionService import delta.codecharacter.server.code.latest_code.LatestCodeService import delta.codecharacter.server.code.locked_code.LockedCodeService +import delta.codecharacter.server.daily_challenge.DailyChallengeService +import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchEntity +import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchRepository +import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchVerdictEnum import delta.codecharacter.server.exception.CustomException import delta.codecharacter.server.game.GameService import delta.codecharacter.server.game.GameStatusEnum import delta.codecharacter.server.game_map.latest_map.LatestMapService import delta.codecharacter.server.game_map.locked_map.LockedMapService import delta.codecharacter.server.game_map.map_revision.MapRevisionService +import delta.codecharacter.server.logic.validation.MapValidator import delta.codecharacter.server.logic.verdict.VerdictAlgorithm import delta.codecharacter.server.notifications.NotificationService import delta.codecharacter.server.user.public_user.PublicUserService import delta.codecharacter.server.user.rating_history.RatingHistoryService +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus @@ -29,6 +39,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.stereotype.Service import java.math.BigDecimal +import java.time.Duration import java.time.Instant import java.util.UUID @@ -46,10 +57,15 @@ class MatchService( @Autowired private val verdictAlgorithm: VerdictAlgorithm, @Autowired private val ratingHistoryService: RatingHistoryService, @Autowired private val notificationService: NotificationService, + @Autowired private val dailyChallengeService: DailyChallengeService, + @Autowired private val dailyChallengeMatchRepository: DailyChallengeMatchRepository, @Autowired private val jackson2ObjectMapperBuilder: Jackson2ObjectMapperBuilder, @Autowired private val simpMessagingTemplate: SimpMessagingTemplate, + @Autowired private val mapValidator: MapValidator, + @Autowired private val autoMatchRepository: AutoMatchRepository ) { private var mapper: ObjectMapper = jackson2ObjectMapperBuilder.build() + private val logger: Logger = LoggerFactory.getLogger(MatchService::class.java) private fun createSelfMatch(userId: UUID, codeRevisionId: UUID?, mapRevisionId: UUID?) { val code: String @@ -95,15 +111,13 @@ class MatchService( gameService.sendGameRequest(game, code, LanguageEnum.valueOf(language.name), map) } - fun createDualMatch(userId: UUID, opponentUsername: String) { + fun createDualMatch(userId: UUID, opponentUsername: String, mode: MatchModeEnum): UUID { val publicUser = publicUserService.getPublicUser(userId) val publicOpponent = publicUserService.getPublicUserByUsername(opponentUsername) val opponentId = publicOpponent.userId - if (userId == opponentId) { throw CustomException(HttpStatus.BAD_REQUEST, "You cannot play against yourself") } - val (userLanguage, userCode) = lockedCodeService.getLockedCode(userId) val userMap = lockedMapService.getLockedMap(userId) @@ -119,7 +133,7 @@ class MatchService( MatchEntity( id = matchId, games = listOf(game1, game2), - mode = MatchModeEnum.MANUAL, + mode = mode, verdict = MatchVerdictEnum.TIE, createdAt = Instant.now(), totalPoints = 0, @@ -130,8 +144,54 @@ class MatchService( gameService.sendGameRequest(game1, userCode, userLanguage, opponentMap) gameService.sendGameRequest(game2, opponentCode, opponentLanguage, userMap) + if (mode == MatchModeEnum.AUTO) { + logger.info( + "Auto match started between ${match.player1.username} and ${match.player2.username}" + ) + } + return matchId } + fun createDCMatch(userId: UUID, dailyChallengeMatchRequestDto: DailyChallengeMatchRequestDto) { + val (_, chall, challType, _, completionStatus) = + dailyChallengeService.getDailyChallengeByDateForUser(userId) + if (completionStatus != null && completionStatus) { + throw CustomException( + HttpStatus.BAD_REQUEST, "You have already completed your daily challenge" + ) + } + val dc = dailyChallengeService.getDailyChallengeByDate() + val (value, _) = dailyChallengeMatchRequestDto + val language: LanguageEnum + val map: String + val code: String + when (challType) { + ChallengeTypeDto.CODE -> { // code as question and map as answer + mapValidator.validateMap(value) + code = chall.cpp.toString() + language = LanguageEnum.CPP + map = value + } + ChallengeTypeDto.MAP -> { + map = dc.map + language = LanguageEnum.valueOf(dailyChallengeMatchRequestDto.language.toString()) + code = value + } + } + val matchId = UUID.randomUUID() + val game = gameService.createGame(matchId) + val user = publicUserService.getPublicUser(userId) + val match = + DailyChallengeMatchEntity( + id = matchId, + verdict = DailyChallengeMatchVerdictEnum.STARTED, + createdAt = Instant.now(), + user = user, + game = game + ) + dailyChallengeMatchRepository.save(match) + gameService.sendGameRequest(game, code, language, map) + } fun createMatch(userId: UUID, createMatchRequestDto: CreateMatchRequestDto) { when (createMatchRequestDto.mode) { MatchModeDto.SELF -> { @@ -142,7 +202,27 @@ class MatchService( if (createMatchRequestDto.opponentUsername == null) { throw CustomException(HttpStatus.BAD_REQUEST, "Opponent ID is required") } - createDualMatch(userId, createMatchRequestDto.opponentUsername!!) + createDualMatch(userId, createMatchRequestDto.opponentUsername!!, MatchModeEnum.MANUAL) + } + else -> { + throw CustomException(HttpStatus.BAD_REQUEST, "MatchMode Is Not Correct") + } + } + } + + fun createAutoMatch() { + val topNUsers = publicUserService.getTopNUsers() + val userIds = topNUsers.map { it.userId } + val usernames = topNUsers.map { it.username } + logger.info("Auto matches started for users: $usernames") + autoMatchRepository.deleteAll() + userIds.forEachIndexed { i, userId -> + run { + for (j in i + 1 until userIds.size) { + val opponentUsername = usernames[j] + val matchId = createDualMatch(userId, opponentUsername, MatchModeEnum.AUTO) + autoMatchRepository.save(AutoMatchEntity(matchId, 0)) + } } } } @@ -170,6 +250,7 @@ class MatchService( PublicUserDto( username = matchEntity.player1.username, name = matchEntity.player1.name, + tier = TierTypeDto.valueOf(matchEntity.player1.tier.name), country = matchEntity.player1.country, college = matchEntity.player1.college, avatarId = matchEntity.player1.avatarId, @@ -178,6 +259,7 @@ class MatchService( PublicUserDto( username = matchEntity.player2.username, name = matchEntity.player2.name, + tier = TierTypeDto.valueOf(matchEntity.player2.tier.name), country = matchEntity.player2.country, college = matchEntity.player2.college, avatarId = matchEntity.player2.avatarId, @@ -186,6 +268,37 @@ class MatchService( } } + private fun mapDailyChallengeMatchEntitiesToDtos( + dailyChallengeMatchEntities: List + ): List { + return dailyChallengeMatchEntities.map { entity -> + MatchDto( + id = entity.id, + matchMode = MatchModeDto.valueOf("DAILYCHALLENGE"), + matchVerdict = VerdictDto.valueOf(entity.verdict.name), + createdAt = entity.createdAt, + games = + setOf( + GameDto( + id = entity.game.id, + destruction = BigDecimal(entity.game.destruction), + coinsUsed = entity.game.coinsUsed, + status = GameStatusDto.valueOf(entity.game.status.name) + ) + ), + user1 = + PublicUserDto( + username = entity.user.username, + name = entity.user.name, + tier = TierTypeDto.valueOf(entity.user.tier.name), + country = entity.user.country, + college = entity.user.college, + avatarId = entity.user.avatarId, + ), + ) + } + } + fun getTopMatches(): List { val matches = matchRepository.findTop10ByOrderByTotalPointsDesc() return mapMatchEntitiesToDtos(matches) @@ -193,18 +306,141 @@ class MatchService( fun getUserMatches(userId: UUID): List { val publicUser = publicUserService.getPublicUser(userId) - val matches = matchRepository.findByPlayer1OrderByCreatedAtDesc(publicUser) - return mapMatchEntitiesToDtos(matches) + val matches = + matchRepository.findByPlayer1OrderByCreatedAtDesc(publicUser).filter { match -> + match.mode != MatchModeEnum.AUTO + } + val dcMatches = + dailyChallengeMatchRepository.findByUserOrderByCreatedAtDesc(publicUser).takeWhile { + Duration.between(it.createdAt, Instant.now()).toHours() < 24 && + it.verdict != DailyChallengeMatchVerdictEnum.STARTED + } + return mapDailyChallengeMatchEntitiesToDtos(dcMatches) + mapMatchEntitiesToDtos(matches) } @RabbitListener(queues = ["gameStatusUpdateQueue"], ackMode = "AUTO") fun receiveGameResult(gameStatusUpdateJson: String) { val updatedGame = gameService.updateGameStatus(gameStatusUpdateJson) + val matchId = updatedGame.matchId + if (matchRepository.findById(matchId).isPresent) { + val match = matchRepository.findById(updatedGame.matchId).get() + if (match.mode != MatchModeEnum.AUTO && match.games.first().id == updatedGame.id) { + simpMessagingTemplate.convertAndSend( + "/updates/${match.player1.userId}", + mapper.writeValueAsString( + GameDto( + id = updatedGame.id, + destruction = BigDecimal(updatedGame.destruction), + coinsUsed = updatedGame.coinsUsed, + status = GameStatusDto.valueOf(updatedGame.status.name), + ) + ) + ) + } + if (match.mode != MatchModeEnum.SELF && + match.games.all { game -> + game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR + } + ) { - val match = matchRepository.findById(updatedGame.matchId).get() - if (match.mode != MatchModeEnum.AUTO && match.games.first().id == updatedGame.id) { + if (match.mode == MatchModeEnum.AUTO) { + if (match.games.any { game -> game.status == GameStatusEnum.EXECUTE_ERROR }) { + val autoMatch = autoMatchRepository.findById(match.id).get() + if (autoMatch.tries < 2) { + autoMatchRepository.delete(autoMatch) + val newMatchId = + createDualMatch(match.player1.userId, match.player2.username, MatchModeEnum.AUTO) + autoMatchRepository.save(AutoMatchEntity(newMatchId, autoMatch.tries + 1)) + return + } + } + } + + val player1Game = match.games.first() + val player2Game = match.games.last() + val verdict = + verdictAlgorithm.getVerdict( + player1Game.status == GameStatusEnum.EXECUTE_ERROR, + player1Game.coinsUsed, + player1Game.destruction, + player2Game.status == GameStatusEnum.EXECUTE_ERROR, + player2Game.coinsUsed, + player2Game.destruction + ) + val finishedMatch = match.copy(verdict = verdict) + val (newUserRating, newOpponentRating) = + ratingHistoryService.updateRating(match.player1.userId, match.player2.userId, verdict) + if (match.mode == MatchModeEnum.MANUAL) { + if (( + match.player1.tier == TierTypeDto.TIER2 && + match.player2.tier == TierTypeDto.TIER2 + ) || + ( + match.player1.tier == TierTypeDto.TIER_PRACTICE && + match.player2.tier == TierTypeDto.TIER_PRACTICE + ) + ) { + publicUserService.updatePublicRating( + userId = match.player1.userId, + isInitiator = true, + verdict = verdict, + newRating = newUserRating + ) + publicUserService.updatePublicRating( + userId = match.player2.userId, + isInitiator = false, + verdict = verdict, + newRating = newOpponentRating + ) + } + notificationService.sendNotification( + match.player1.userId, + "Match Result", + "${ + when (verdict) { + MatchVerdictEnum.PLAYER1 -> "Won" + MatchVerdictEnum.PLAYER2 -> "Lost" + MatchVerdictEnum.TIE -> "Tied" + } + } against ${match.player2.username}", + ) + } + matchRepository.save(finishedMatch) + + if (match.mode == MatchModeEnum.AUTO) { + if (autoMatchRepository.findAll().all { autoMatch -> + matchRepository.findById(autoMatch.matchId).get().games.all { game -> + game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR + } + } + ) { + val matches = + matchRepository.findByIdIn(autoMatchRepository.findAll().map { it.matchId }) + val userIds = + matches.map { it.player1.userId }.toSet() + + matches.map { it.player2.userId }.toSet() + val newRatings = + ratingHistoryService.updateAndGetAutoMatchRatings(userIds.toList(), matches) + newRatings.forEach { (userId, newRating) -> + publicUserService.updatePublicRating( + userId = userId, + isInitiator = true, + verdict = verdict, + newRating = newRating.rating + ) + } + logger.info("LeaderBoard Tier Promotion and Demotion started") + publicUserService.promoteTiers() + } + logger.info( + "Match between ${match.player1.username} and ${match.player2.username} completed with verdict $verdict" + ) + } + } + } else if (dailyChallengeMatchRepository.findById(matchId).isPresent) { + val match = dailyChallengeMatchRepository.findById(matchId).get() simpMessagingTemplate.convertAndSend( - "/updates/${match.player1.userId}", + "/updates/${match.user.userId}", mapper.writeValueAsString( GameDto( id = updatedGame.id, @@ -214,53 +450,26 @@ class MatchService( ) ) ) - } - if (match.mode != MatchModeEnum.SELF && - match.games.all { game -> - game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR - } - ) { - val player1Game = match.games.first() - val player2Game = match.games.last() - val verdict = - verdictAlgorithm.getVerdict( - player1Game.status == GameStatusEnum.EXECUTE_ERROR, - player1Game.coinsUsed, - player1Game.destruction, - player2Game.status == GameStatusEnum.EXECUTE_ERROR, - player2Game.coinsUsed, - player2Game.destruction - ) - val finishedMatch = match.copy(verdict = verdict) - val (newUserRating, newOpponentRating) = - ratingHistoryService.updateRating(match.player1.userId, match.player2.userId, verdict) - - publicUserService.updatePublicRating( - userId = match.player1.userId, - isInitiator = true, - verdict = verdict, - newRating = newUserRating - ) - publicUserService.updatePublicRating( - userId = match.player2.userId, - isInitiator = false, - verdict = verdict, - newRating = newOpponentRating - ) - - if (match.mode == MatchModeEnum.MANUAL) { + if (updatedGame.status != GameStatusEnum.EXECUTING) { + val updatedMatch = + match.copy( + verdict = + dailyChallengeService.completeDailyChallenge(updatedGame, match.user.userId) + ) notificationService.sendNotification( - match.player1.userId, - "Match Result", - "${when (verdict) { - MatchVerdictEnum.PLAYER1 -> "Won" - MatchVerdictEnum.PLAYER2 -> "Lost" - MatchVerdictEnum.TIE -> "Tied" - }} against ${match.player2.username}", + match.user.userId, + title = "Daily Challenge Results", + content = + when (updatedMatch.verdict) { + DailyChallengeMatchVerdictEnum.SUCCESS -> "Successfully completed challenge" + DailyChallengeMatchVerdictEnum.FAILURE -> "Failed to complete challenge" + else -> { + "Some error occurred. Try again!" + } + } ) + dailyChallengeMatchRepository.save(updatedMatch) } - - matchRepository.save(finishedMatch) } } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Attacker.kt b/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Attacker.kt index 66909e62..c3708a59 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Attacker.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Attacker.kt @@ -9,4 +9,5 @@ data class Attacker( @field:JsonProperty("attack_power", required = true) val attackPower: Int, @field:JsonProperty("speed", required = true) val speed: Int, @field:JsonProperty("price", required = true) val price: Int, + @field:JsonProperty("is_aerial", required = true) val aerial: Int, ) diff --git a/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Defender.kt b/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Defender.kt index fe8e558b..1852a86a 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Defender.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/params/game_entities/Defender.kt @@ -8,4 +8,5 @@ data class Defender( @field:JsonProperty("range", required = true) val range: Int, @field:JsonProperty("attack_power", required = true) val attackPower: Int, @field:JsonProperty("price", required = true) val price: Int, + @field:JsonProperty("is_aerial", required = true) val aerial: Int, ) diff --git a/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt b/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt new file mode 100644 index 00000000..6900bba2 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt @@ -0,0 +1,29 @@ +package delta.codecharacter.server.schedulers + +import delta.codecharacter.server.match.MatchService +import delta.codecharacter.server.user.public_user.PublicUserService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +class SchedulingService( + @Autowired private val publicUserService: PublicUserService, + @Autowired private val matchService: MatchService +) { + private val logger: Logger = LoggerFactory.getLogger(SchedulingService::class.java) + + @Scheduled(cron = "\${environment.registration-time}", zone = "GMT+5:30") + fun updateTempLeaderboard() { + logger.info("Practice phase ended!!") + publicUserService.resetRatingsAfterPracticePhase() + publicUserService.updateLeaderboardAfterPracticePhase() + } + + @Scheduled(cron = "\${environment.promote-demote-time}", zone = "GMT+5:30") + fun createAutoMatch() { + matchService.createAutoMatch() + } +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/seeders/DailyChallengeObject.kt b/server/src/main/kotlin/delta/codecharacter/server/seeders/DailyChallengeObject.kt new file mode 100644 index 00000000..1373d07f --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/seeders/DailyChallengeObject.kt @@ -0,0 +1,17 @@ +package delta.codecharacter.server.seeders + +import com.fasterxml.jackson.annotation.JsonProperty +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.dtos.DailyChallengeObjectDto + +data class DailyChallengeObject( + @field:JsonProperty("day") val day: Int, + @field:JsonProperty("challName") val challName: String, + @field:JsonProperty("challType") val challType: ChallengeTypeDto, + @field:JsonProperty("chall") val chall: DailyChallengeObjectDto, + @field:JsonProperty("description") val description: String?, + @field:JsonProperty("perfectScore") val perfectScore: Int, + @field:JsonProperty("numberOfCompletions") val numberOfCompletions: Int, + @field:JsonProperty("toleratedDestruction") val toleratedDestruction: Int, + @field:JsonProperty("map") val map: String +) diff --git a/server/src/main/kotlin/delta/codecharacter/server/seeders/DailyChallengeSeeder.kt b/server/src/main/kotlin/delta/codecharacter/server/seeders/DailyChallengeSeeder.kt new file mode 100644 index 00000000..d2457bd5 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/seeders/DailyChallengeSeeder.kt @@ -0,0 +1,59 @@ +package delta.codecharacter.server.seeders + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import delta.codecharacter.server.daily_challenge.DailyChallengeEntity +import delta.codecharacter.server.daily_challenge.DailyChallengeRepository +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +class DailyChallengeSeeder { + + @Autowired private lateinit var dailyChallengeRepository: DailyChallengeRepository + + private val logger: Logger = LoggerFactory.getLogger(DailyChallengeSeeder::class.java) + @EventListener(ApplicationReadyEvent::class) + fun seedDailyChallenges() { + + if (dailyChallengeRepository.findAll().isEmpty()) { + logger.info("Seeding daily_challenges") + + val jsonString = this::class.java.classLoader.getResource("dcConstants.json")?.readText() + if (!jsonString.isNullOrEmpty()) { + val objectMapper = jacksonObjectMapper() + val dcs: List = objectMapper.readValue(jsonString) + var dcEntities: List = listOf() + dcs.forEach { + val id = UUID.randomUUID() + dcEntities = + dcEntities.plus( + DailyChallengeEntity( + id = id, + day = it.day, + chall = it.chall, + challName = it.challName, + challType = it.challType, + description = it.description, + perfectScore = it.perfectScore, + numberOfCompletions = it.numberOfCompletions, + toleratedDestruction = it.toleratedDestruction, + map = it.map, + ) + ) + } + dailyChallengeRepository.saveAll(dcEntities) + logger.info("Seeding Daily-Challenges Completed") + } else { + logger.error("dcConstants.json is empty or doesn't exist") + } + } else { + logger.info("Daily Challenges seeded already") + } + } +} diff --git a/server/src/main/kotlin/delta/codecharacter/server/sendgrid/SendGridService.kt b/server/src/main/kotlin/delta/codecharacter/server/sendgrid/SendGridService.kt index b29eae27..0ec4c9eb 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/sendgrid/SendGridService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/sendgrid/SendGridService.kt @@ -5,6 +5,7 @@ import com.sendgrid.Request import com.sendgrid.Response import com.sendgrid.SendGrid import com.sendgrid.helpers.mail.Mail +import com.sendgrid.helpers.mail.objects.Content import com.sendgrid.helpers.mail.objects.Email import delta.codecharacter.server.exception.CustomException import org.slf4j.Logger @@ -22,28 +23,33 @@ class SendGridService { @Autowired private lateinit var sendGrid: SendGrid - @Value("\${spring.sendgrid.template-id}") private lateinit var templateId: String - @Value("\${spring.sendgrid.sender-email}") private lateinit var senderEmail: String + // @Value("\${spring.sendgrid.template-id}") private lateinit var templateId: String @Value("\${base-url}") private lateinit var baseUrl: String fun activateUserEmail(userId: UUID, token: String, name: String, email: String) { val link = "$baseUrl#/activate?id=$userId&token=$token" - val linkName = "User Activation link" - val message = "Please click the button to activate your account" + val linkName = "User Activation" + val message = "You are just one step away from activating your CodeCharacter account" + val disregardMessage = "If you did not create an account with us,please disregard this email" val buttonName = "Activate" val subjectInfo = "CodeCharacter Account Activation Link" - sendTemplateEmail(email, name, linkName, link, message, buttonName, subjectInfo) + sendTemplateEmail( + email, name, linkName, link, message, disregardMessage, buttonName, subjectInfo + ) } fun resetPasswordEmail(userId: UUID, token: String, name: String, email: String) { val link = "$baseUrl#/reset-password?id=$userId&token=$token" val linkName = "Reset-Password link" val message = "Please click the button to reset your password" + val disregardMessage = "If you did not request a password reset,please ignore this email." val buttonName = "Reset Password" val subjectInfo = "CodeCharacter Reset-Password Link" - sendTemplateEmail(email, name, linkName, link, message, buttonName, subjectInfo) + sendTemplateEmail( + email, name, linkName, link, message, disregardMessage, buttonName, subjectInfo + ) } fun sendTemplateEmail( @@ -52,22 +58,261 @@ class SendGridService { linkName: String, link: String, message: String, + disregardMessage: String, buttonName: String, subjectInfo: String ) { - val mail = Mail() - val personalization = DynamicTemplatePersonalization() - personalization.addTo(Email(emailTo)) - mail.setFrom(Email(senderEmail)) - mail.setSubject(subjectInfo) - personalization.addDynamicTemplateData("name", name) - personalization.addDynamicTemplateData("link_name", linkName) - personalization.addDynamicTemplateData("message", message) - personalization.addDynamicTemplateData("link", link) - personalization.addDynamicTemplateData("button_name", buttonName) - personalization.addDynamicTemplateData("subject", subjectInfo) - mail.addPersonalization(personalization) - mail.setTemplateId(templateId) + // Using DynamicTemplates are preferred but we dont have creds to access that so we used this :( + // val mail = Mail() + // val personalization = DynamicTemplatePersonalization() + // personalization.addTo(Email(emailTo)) + // mail.setFrom(Email(senderEmail)) + // mail.setSubject(subjectInfo) + // personalization.addDynamicTemplateData("name", name) + // personalization.addDynamicTemplateData("link_name", linkName) + // personalization.addDynamicTemplateData("message", message) + // personalization.addDynamicTemplateData("link", link) + // personalization.addDynamicTemplateData("button_name", buttonName) + // personalization.addDynamicTemplateData("subject", subjectInfo) + // mail.addPersonalization(personalization) + // mail.setTemplateId(templateId) + val emailContent = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "

\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Email not displaying correctly? VIEW IT in your browser.
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
$linkName
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Hi $name,
\n" + + "

\n" + + "
$message,
\n" + + "

\n" + + "
$disregardMessage
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " $buttonName\n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
With Regards!
\n" + + "
CodeCharacter
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
CodeCharatcer-2023
\n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + " " + val content = Content("text/html", emailContent) + val mail = Mail(Email(senderEmail), subjectInfo, Email(emailTo), content) val request = Request() try { request.apply { diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/UserService.kt b/server/src/main/kotlin/delta/codecharacter/server/user/UserService.kt index 2d8e494c..a160e528 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/UserService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/UserService.kt @@ -7,7 +7,9 @@ import delta.codecharacter.server.exception.CustomException import delta.codecharacter.server.user.activate_user.ActivateUserService import delta.codecharacter.server.user.public_user.PublicUserService import delta.codecharacter.server.user.rating_history.RatingHistoryService +import org.bson.json.JsonObject import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Lazy import org.springframework.dao.DuplicateKeyException import org.springframework.http.HttpStatus @@ -16,6 +18,10 @@ import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.stereotype.Service +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse import java.util.Optional import java.util.UUID @@ -28,6 +34,7 @@ class UserService( ) : UserDetailsService { @Lazy @Autowired private lateinit var passwordEncoder: BCryptPasswordEncoder + @Value("\${environment.reCaptcha-key}") private lateinit var secretKey: String override fun loadUserByUsername(email: String?): UserEntity { if (email == null) { @@ -109,9 +116,34 @@ class UserService( } fun registerUser(registerUserRequestDto: RegisterUserRequestDto) { - val (username, name, email, password, passwordConfirmation, country, college, avatarId) = + val ( + username, + name, + email, + password, + passwordConfirmation, + country, + college, + avatarId, + recaptchaCode + ) = registerUserRequestDto + if (username.trim().length < 5) { + throw CustomException(HttpStatus.BAD_REQUEST, "Username must be minimum 5 characters long") + } + if (name.trim().length < 5) { + throw CustomException(HttpStatus.BAD_REQUEST, "Name must be minimum 5 characters long") + } + if (avatarId !in 0..19) { + throw CustomException(HttpStatus.BAD_REQUEST, "Selected Avatar is invalid") + } + if (college.trim().isEmpty()) { + throw CustomException(HttpStatus.BAD_REQUEST, "College can not be empty") + } + if (country.trim().isEmpty()) { + throw CustomException(HttpStatus.BAD_REQUEST, "Country can not be empty") + } if (password != passwordConfirmation) { throw CustomException( HttpStatus.BAD_REQUEST, "Password and password confirmation don't match" @@ -122,6 +154,9 @@ class UserService( throw CustomException(HttpStatus.BAD_REQUEST, "Username already taken") } + if (!verifyReCaptcha(recaptchaCode)) + throw CustomException(HttpStatus.BAD_REQUEST, "Invalid ReCaptcha") + val userId = UUID.randomUUID() try { createUserWithPassword(userId, password, email) @@ -133,6 +168,28 @@ class UserService( } } + fun verifyReCaptcha(reCaptchaResponse: String): Boolean { + val url = + "https://www.google.com/recaptcha/api/siteverify?secret=$secretKey&response=$reCaptchaResponse" + try { + val client = HttpClient.newBuilder().build() + val request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + val json = JsonObject(response.body()).toBsonDocument() + return ( + json.getBoolean("success").value && + (json.getDouble("score").value.compareTo(0.5) >= 0) + ) + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + fun activateUser(userId: UUID, token: String) { activateUserService.processActivationToken(userId, token) val user = userRepository.findFirstById((userId)).get() @@ -143,6 +200,12 @@ class UserService( fun completeUserProfile(userId: UUID, completeProfileRequestDto: CompleteProfileRequestDto) { val (username, name, country, college, avatarId) = completeProfileRequestDto val user = userRepository.findFirstById(userId).get() + if (user.isProfileComplete) { + throw CustomException(HttpStatus.BAD_REQUEST, "User profile is already complete") + } + if (!publicUserService.isUsernameUnique(username)) { + throw CustomException(HttpStatus.BAD_REQUEST, "Username already taken") + } publicUserService.create(userId, username, name, country, college, avatarId) userRepository.save( user.copy( diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/DailyChallengeHistory.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/DailyChallengeHistory.kt new file mode 100644 index 00000000..0ab54e68 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/DailyChallengeHistory.kt @@ -0,0 +1,5 @@ +package delta.codecharacter.server.user.public_user + +import delta.codecharacter.server.daily_challenge.DailyChallengeEntity + +data class DailyChallengeHistory(val score: Double, val dailyChallenge: DailyChallengeEntity) diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt index 045744dc..32413839 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.user.public_user +import delta.codecharacter.dtos.TierTypeDto import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Document @@ -13,9 +14,13 @@ data class PublicUserEntity( val country: String, val college: String, val avatarId: Int, + val tier: TierTypeDto, + val tutorialLevel: Int, val rating: Double, val wins: Int, val losses: Int, val ties: Int, val isActivated: Boolean = true, + val score: Double, + val dailyChallengeHistory: HashMap ) diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt index d09d43d4..8a6d96e9 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt @@ -1,9 +1,15 @@ package delta.codecharacter.server.user.public_user +import delta.codecharacter.dtos.TierTypeDto +import org.springframework.data.domain.PageRequest import org.springframework.data.mongodb.repository.MongoRepository import java.util.Optional import java.util.UUID interface PublicUserRepository : MongoRepository { fun findByUsername(username: String): Optional + + fun findTopnByOrderByRatingDesc(pageRequest: PageRequest): List + + fun findAllByTier(tier: TierTypeDto?, pageRequest: PageRequest): List } diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt index 5be187c1..8b2b5f90 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt @@ -1,13 +1,20 @@ package delta.codecharacter.server.user.public_user import delta.codecharacter.dtos.CurrentUserProfileDto +import delta.codecharacter.dtos.DailyChallengeLeaderBoardResponseDto import delta.codecharacter.dtos.LeaderboardEntryDto import delta.codecharacter.dtos.PublicUserDto +import delta.codecharacter.dtos.TierTypeDto +import delta.codecharacter.dtos.TutorialUpdateTypeDto import delta.codecharacter.dtos.UpdateCurrentUserProfileDto import delta.codecharacter.dtos.UserStatsDto +import delta.codecharacter.server.daily_challenge.DailyChallengeEntity import delta.codecharacter.server.exception.CustomException import delta.codecharacter.server.match.MatchVerdictEnum +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.http.HttpStatus @@ -18,6 +25,11 @@ import java.util.UUID @Service class PublicUserService(@Autowired private val publicUserRepository: PublicUserRepository) { + @Value("\${environment.no-of-tutorial-level}") private lateinit var totalTutorialLevels: Number + @Value("\${environment.no-of-tier-1-players}") private var tier1Players: Number = 1 + @Value("\${environment.no-of-players-for-promotion}") private var topPlayer: Number = 1 + private val logger: Logger = LoggerFactory.getLogger(PublicUserService::class.java) + fun create( userId: UUID, username: String, @@ -38,18 +50,83 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR wins = 0, losses = 0, ties = 0, + score = 0.0, + tier = TierTypeDto.TIER_PRACTICE, + tutorialLevel = 1, + dailyChallengeHistory = HashMap() ) publicUserRepository.save(publicUser) } - fun getLeaderboard(page: Int?, size: Int?): List { - val pageRequest = PageRequest.of(page ?: 0, size ?: 10, Sort.by(Sort.Direction.DESC, "rating")) - return publicUserRepository.findAll(pageRequest).content.map { + fun updateLeaderboardAfterPracticePhase() { + val publicUsers = publicUserRepository.findAll() + publicUsers.forEachIndexed { index, user -> + if (index < tier1Players.toInt()) { + publicUserRepository.save(user.copy(tier = TierTypeDto.TIER1)) + } else { + publicUserRepository.save(user.copy(tier = TierTypeDto.TIER2)) + } + } + logger.info("Leaderboard tier set during the start of game phase") + } + + fun resetRatingsAfterPracticePhase() { + logger.info("Reset ratings after practice phase starts") + val users = publicUserRepository.findAll() + users.forEach { user -> + publicUserRepository.save(user.copy(rating = 1500.0, wins = 0, ties = 0, losses = 0)) + } + logger.info("Reset ratings after practice phase has ended") + } + + fun promoteTiers() { + val topPlayersInTier2 = + publicUserRepository.findAllByTier( + TierTypeDto.TIER2, + PageRequest.of(0, topPlayer.toInt(), Sort.by(Sort.Order.desc("rating"))) + ) + val bottomPlayersInTier1 = + publicUserRepository.findAllByTier( + TierTypeDto.TIER1, + PageRequest.of(0, topPlayer.toInt(), Sort.by(Sort.Order.asc("rating"))) + ) + topPlayersInTier2.forEach { users -> + val updatedToTier1User = publicUserRepository.save(users.copy(tier = TierTypeDto.TIER1)) + if (updatedToTier1User.tier == TierTypeDto.TIER1) { + logger.info("UserName ${updatedToTier1User.username} got promoted to TIER1") + } else { + logger.error( + "Error occurred while updating ${updatedToTier1User.username} (UserName) to TIER1" + ) + } + } + bottomPlayersInTier1.forEach { users -> + val updateToTier2User = publicUserRepository.save(users.copy(tier = TierTypeDto.TIER2)) + if (updateToTier2User.tier == TierTypeDto.TIER2) { + logger.info("UserName ${updateToTier2User.username} got demoted to TIER2") + } else { + logger.error( + "Error occurred while updating ${updateToTier2User.username} (UserName) to TIER2" + ) + } + } + } + + fun getLeaderboard(page: Int?, size: Int?, tier: TierTypeDto?): List { + val pageRequest = PageRequest.of(page ?: 0, size ?: 10, Sort.by(Sort.Order.desc("rating"))) + val publicUsers = + if (tier == null) { + publicUserRepository.findAll(pageRequest).content + } else { + publicUserRepository.findAllByTier(tier, pageRequest) + } + return publicUsers.map { LeaderboardEntryDto( user = PublicUserDto( username = it.username, name = it.name, + tier = TierTypeDto.valueOf(it.tier.name), country = it.country, college = it.college, avatarId = it.avatarId, @@ -59,8 +136,20 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR rating = BigDecimal(it.rating), wins = it.wins, losses = it.losses, - ties = it.ties, - ) + ties = it.ties + ), + ) + } + } + + fun getDailyChallengeLeaderboard( + page: Int?, + size: Int? + ): List { + val pageRequest = PageRequest.of(page ?: 0, size ?: 10, Sort.by(Sort.Direction.DESC, "score")) + return publicUserRepository.findAll(pageRequest).content.map { + DailyChallengeLeaderBoardResponseDto( + userName = it.username, score = BigDecimal(it.score), avatarId = it.avatarId ) } } @@ -74,22 +163,80 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR name = user.name, country = user.country, college = user.college, + tutorialLevel = user.tutorialLevel, avatarId = user.avatarId, + isTutorialComplete = user.tutorialLevel == totalTutorialLevels.toInt() ) } fun updateUserProfile(userId: UUID, updateCurrentUserProfileDto: UpdateCurrentUserProfileDto) { val user = publicUserRepository.findById(userId).get() + + if (updateCurrentUserProfileDto.name != null && + updateCurrentUserProfileDto.name!!.trim().length < 5 + ) { + throw CustomException(HttpStatus.BAD_REQUEST, "Name must be minimum 5 characters") + } + + if (updateCurrentUserProfileDto.country != null && + updateCurrentUserProfileDto.country!!.trim().isEmpty() + ) { + throw CustomException(HttpStatus.BAD_REQUEST, "Country can not be an empty") + } + + if (updateCurrentUserProfileDto.college != null && + updateCurrentUserProfileDto.college!!.trim().isEmpty() + ) { + throw CustomException(HttpStatus.BAD_REQUEST, "College can not be an empty") + } + if (updateCurrentUserProfileDto.avatarId != null && + updateCurrentUserProfileDto.avatarId!! !in 0..19 + ) { + throw CustomException(HttpStatus.BAD_REQUEST, "Selected Avatar is invalid") + } + val updatedUser = user.copy( name = updateCurrentUserProfileDto.name ?: user.name, country = updateCurrentUserProfileDto.country ?: user.country, college = updateCurrentUserProfileDto.college ?: user.college, - avatarId = updateCurrentUserProfileDto.avatarId ?: user.avatarId + avatarId = updateCurrentUserProfileDto.avatarId ?: user.avatarId, + tutorialLevel = + updateTutorialLevel( + updateCurrentUserProfileDto.updateTutorialLevel, user.tutorialLevel + ) ) publicUserRepository.save(updatedUser) } + fun updateTutorialLevel(updateTutorialType: TutorialUpdateTypeDto?, tutorialLevel: Int): Int { + var updatedTutorialLevel = tutorialLevel + when (updateTutorialType) { + TutorialUpdateTypeDto.NEXT -> { + if (tutorialLevel >= totalTutorialLevels.toInt()) { + return tutorialLevel + } + updatedTutorialLevel += 1 + } + TutorialUpdateTypeDto.PREVIOUS -> { + if (tutorialLevel <= 1) { + return 1 + } + updatedTutorialLevel -= 1 + } + TutorialUpdateTypeDto.SKIP -> { + updatedTutorialLevel = totalTutorialLevels.toInt() + } + TutorialUpdateTypeDto.RESET -> { + updatedTutorialLevel = 1 + } + else -> { + return tutorialLevel + } + } + return updatedTutorialLevel + } + fun updatePublicRating( userId: UUID, isInitiator: Boolean, @@ -130,4 +277,17 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR fun isUsernameUnique(username: String): Boolean { return publicUserRepository.findByUsername(username).isEmpty } + + fun updateDailyChallengeScore(userId: UUID, score: Double, dailyChallenge: DailyChallengeEntity) { + val user = publicUserRepository.findById(userId).get() + val current = user.dailyChallengeHistory + current[dailyChallenge.day] = DailyChallengeHistory(score, dailyChallenge) + val updatedUser = user.copy(score = user.score + score, dailyChallengeHistory = current) + publicUserRepository.save(updatedUser) + } + + fun getTopNUsers(): List { + val pageRequest = PageRequest.of(0, tier1Players.toInt(), Sort.by("rating")) + return publicUserRepository.findTopnByOrderByRatingDesc(pageRequest) + } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt b/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt index a838d5f7..7de0ada7 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt @@ -3,6 +3,7 @@ package delta.codecharacter.server.user.rating_history import delta.codecharacter.dtos.RatingHistoryDto import delta.codecharacter.server.logic.rating.GlickoRating import delta.codecharacter.server.logic.rating.RatingAlgorithm +import delta.codecharacter.server.match.MatchEntity import delta.codecharacter.server.match.MatchVerdictEnum import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -93,4 +94,73 @@ class RatingHistoryService( return Pair(newUserRating.rating, newOpponentRating.rating) } + + private fun getNewRatingAfterAutoMatches( + userId: UUID, + userRatings: Map, + autoMatches: List + ): GlickoRating { + val userAsInitiatorMatches = autoMatches.filter { it.player1.userId == userId } + val userAsOpponentMatches = autoMatches.filter { it.player2.userId == userId } + + val usersWeightedRatingDeviations = + userRatings + .map { + it.key to + ratingAlgorithm.getWeightedRatingDeviationSinceLastCompetition( + it.value.ratingDeviation, it.value.validFrom + ) + } + .toMap() + + val ratingsForUserAsPlayer1 = + userAsInitiatorMatches.map { match -> + GlickoRating(match.player2.rating, usersWeightedRatingDeviations[match.player2.userId]!!) + } + val verdictsForUserAsPlayer1 = + userAsInitiatorMatches.map { match -> convertVerdictToMatchResult(match.verdict) } + + val ratingsForUserAsPlayer2 = + userAsOpponentMatches.map { match -> + GlickoRating(match.player1.rating, usersWeightedRatingDeviations[match.player1.userId]!!) + } + val verdictsForUserAsPlayer2 = + userAsOpponentMatches.map { match -> 1.0 - convertVerdictToMatchResult(match.verdict) } + + val ratings = ratingsForUserAsPlayer1 + ratingsForUserAsPlayer2 + val verdicts = verdictsForUserAsPlayer1 + verdictsForUserAsPlayer2 + + return ratingAlgorithm.calculateNewRating( + GlickoRating(userRatings[userId]!!.rating, usersWeightedRatingDeviations[userId]!!), + ratings, + verdicts + ) + } + + fun updateAndGetAutoMatchRatings( + userIds: List, + matches: List + ): Map { + val userRatings = + userIds.associateWith { userId -> + ratingHistoryRepository.findFirstByUserIdOrderByValidFromDesc(userId) + } + val newRatings = + userIds.associateWith { userId -> + getNewRatingAfterAutoMatches(userId, userRatings, matches) + } + val currentInstant = Instant.now() + newRatings.forEach { (userId, rating) -> + ratingHistoryRepository.save( + RatingHistoryEntity( + userId = userId, + rating = rating.rating, + ratingDeviation = rating.ratingDeviation, + validFrom = currentInstant + ) + ) + } + + return newRatings + } } diff --git a/server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 84b3c691..47917fa1 100644 --- a/server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -29,6 +29,11 @@ "name": "spring.sendgrid.template-id", "type": "java.lang.String", "description": "Sendgrid Template ID" + }, + { + "name": "environment.reCaptcha-key", + "type": "java.lang.String", + "description": "User reCaptcha key." } ] } diff --git a/server/src/main/resources/application.docker.example.yml b/server/src/main/resources/application.docker.example.yml index 8f855aa9..22a84b45 100644 --- a/server/src/main/resources/application.docker.example.yml +++ b/server/src/main/resources/application.docker.example.yml @@ -3,9 +3,6 @@ spring: mongodb: host: db auto-index-creation: true - mongodb: - embedded: - version: 5.0.5 mvc: pathmatch: matching-strategy: ant_path_matcher @@ -29,6 +26,35 @@ spring: client-id: your-github-client-id client-secret: your-github-client-secret scope: user:email +environment: + event-start-date: YYYY-MM-DDTHH:MM:SSZ + reCaptcha-key: your-recaptcha-key + no-of-tutorial-level: 0 + registration-time: "* * * * * *" + no-of-tier-1-players: 20 + no-of-players-for-promotion: 5 + update-time: "*/20 * * * * *" + +server: + compression: + enabled: true + http2: + enabled: true +# Set below configurations only for production (Done for oauth redirect) +# forward-headers-strategy: framework +# tomcat: +# remoteip: +# host-header: x-forward-for +# protocol-header: x-forwarded-proto +# internal-proxies: .* +# use-relative-redirects: true + + +de: + flapdoodle: + mongodb: + embedded: + version: 6.0.4 debug: false diff --git a/server/src/main/resources/application.example.yml b/server/src/main/resources/application.example.yml index 58f20e8c..233c146c 100644 --- a/server/src/main/resources/application.example.yml +++ b/server/src/main/resources/application.example.yml @@ -3,9 +3,6 @@ spring: mongodb: host: localhost auto-index-creation: true - mongodb: - embedded: - version: 5.0.5 mvc: pathmatch: matching-strategy: ant_path_matcher @@ -27,6 +24,26 @@ spring: client-id: your-github-client-id client-secret: your-github-client-secret scope: user:email +environment: + event-start-date: YYYY-MM-DDTHH:MM:SSZ + reCaptcha-key: your-recaptcha-key + no-of-tutorial-level: 0 + registration-time: "* * * * * *" + no-of-tier-1-players: 20 + no-of-players-for-promotion: 5 + promote-demote-time: "* * */6 * * *" + +server: + compression: + enabled: true + http2: + enabled: true + +de: + flapdoodle: + mongodb: + embedded: + version: 6.0.4 debug: false diff --git a/server/src/main/resources/dcConstants.example.json b/server/src/main/resources/dcConstants.example.json new file mode 100644 index 00000000..28c3a3e4 --- /dev/null +++ b/server/src/main/resources/dcConstants.example.json @@ -0,0 +1,140 @@ +[ + { + "day": 0, + "challName": "Nisi sunt ea culpa est cillum cupidatat officia est.", + "challType": "CODE", + "chall": { + "cpp" : "This is cpp code", + "java" : "This is Java Code", + "python" : "This is python code" + }, + "description": "Sunt ut est ea et dolor minim veniam quis id occaecat adipisicing quis.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map" : "" + }, + { + "day": 1, + "challName": "Exercitation esse ullamco irure quis dolor ut.", + "challType": "CODE", + "chall": { + "cpp" : "This is cpp code", + "java" : "This is Java Code", + "python" : "This is python code" + }, + "description": "Labore adipisicing culpa eiusmod labore mollit nisi ex fugiat nulla et voluptate laborum.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 2, + "challName": "Ex incididunt laboris ex pariatur irure aute qui occaecat ullamco consequat eu mollit fugiat culpa.", + "challType": "CODE", + "chall": { + "cpp" : "This is cpp code", + "java" : "This is Java Code", + "python" : "This is python code" + }, + "description": "Laborum labore in excepteur do.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 3, + "challName": "Sint ut proident ipsum est nostrud nostrud pariatur anim id laboris velit officia est.", + "challType": "MAP", + "chall": { + "image" : "this is map image" + }, + "description": "Esse do sint non enim dolore.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 4, + "challName": "Mollit qui in culpa nostrud veniam commodo fugiat tempor sit dolor duis id non.", + "challType": "MAP", + "chall": { + "image" : "this is map image" + }, + "description": "Et et labore ad ad ex elit et laboris deserunt eiusmod eiusmod reprehenderit eu.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 5, + "challName": "Est qui aliquip esse mollit.", + "challType": "MAP", + "chall": { + "image" : "this is map image" + }, + "description": "Aute voluptate amet aliquip irure.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 6, + "challName": "Duis anim tempor velit do fugiat tempor aute aliqua velit ut reprehenderit.", + "challType": "MAP", + "chall": { + "image" : "this is map image" + }, + "description": "Duis do est Lorem ex veniam.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 7, + "challName": "Labore velit consequat quis irure officia ad qui sint deserunt ea dolore nostrud consequat aute.", + "challType": "CODE", + "chall": { + "cpp" : "This is cpp code", + "java" : "This is Java Code", + "python" : "This is python code" + }, + "description": "Irure qui minim aliqua sunt esse consequat commodo nostrud consectetur.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + }, + { + "day": 8, + "challName": "Ullamco magna Lorem Lorem sit sunt.", + "challType": "MAP", + "chall": {"image" : "this is map image"}, + "description": "Aliquip ad sunt do eu duis nulla deserunt.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "[[0,0,0]]" + }, + { + "day": 9, + "challName": "Est aliquip in ut ex velit.", + "challType": "CODE", + "chall": { + "cpp" : "This is cpp code", + "java" : "This is Java Code", + "python" : "This is python code" + }, + "description": "Commodo ex adipisicing dolore aliqua adipisicing laboris duis ad irure mollit.", + "perfectScore" : 500, + "numberOfCompletions" : 0, + "toleratedDestruction" : 50, + "map": "" + } +] diff --git a/server/src/main/resources/player_code b/server/src/main/resources/player_code new file mode 160000 index 00000000..16fbce4e --- /dev/null +++ b/server/src/main/resources/player_code @@ -0,0 +1 @@ +Subproject commit 16fbce4e8d3df4ba3dedb1c4622651184ac6adb7 diff --git a/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt b/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt index 80f1f651..cb1a4261 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt @@ -1,7 +1,12 @@ package delta.codecharacter.server +import delta.codecharacter.dtos.ChallengeTypeDto +import delta.codecharacter.dtos.DailyChallengeObjectDto +import delta.codecharacter.dtos.TierTypeDto +import delta.codecharacter.server.daily_challenge.DailyChallengeEntity import delta.codecharacter.server.user.LoginType import delta.codecharacter.server.user.UserEntity +import delta.codecharacter.server.user.public_user.DailyChallengeHistory import delta.codecharacter.server.user.public_user.PublicUserEntity import org.springframework.amqp.rabbit.connection.ConnectionFactory import org.springframework.amqp.rabbit.core.RabbitAdmin @@ -28,6 +33,19 @@ class TestAttributes { loginType = LoginType.PASSWORD, isProfileComplete = true, ) + val dailyChallengeCode = + DailyChallengeEntity( + id = UUID.randomUUID(), + day = 0, + challName = "challengeName", + challType = ChallengeTypeDto.CODE, + chall = DailyChallengeObjectDto(cpp = "example cpp code"), + perfectScore = 500, + numberOfCompletions = 2, + toleratedDestruction = 60, + map = "", + description = "description" + ) val publicUser = PublicUserEntity( userId = user.id, @@ -40,6 +58,10 @@ class TestAttributes { wins = 4, losses = 2, ties = 1, + tier = TierTypeDto.TIER_PRACTICE, + score = 0.0, + dailyChallengeHistory = hashMapOf(0 to DailyChallengeHistory(0.0, dailyChallengeCode)), + tutorialLevel = 1 ) } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/code/CodeControllerIntegrationTest.kt b/server/src/test/kotlin/delta/codecharacter/server/code/CodeControllerIntegrationTest.kt index dffd1ce8..7314a773 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/code/CodeControllerIntegrationTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/code/CodeControllerIntegrationTest.kt @@ -3,6 +3,7 @@ package delta.codecharacter.server.code import com.fasterxml.jackson.databind.ObjectMapper import delta.codecharacter.dtos.CodeDto import delta.codecharacter.dtos.CodeRevisionDto +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.CreateCodeRevisionRequestDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto @@ -79,6 +80,7 @@ internal class CodeControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { message = "message", language = LanguageEnum.CPP, userId = TestAttributes.user.id, + codeType = CodeTypeDto.NORMAL, parentRevision = null, createdAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) @@ -104,20 +106,21 @@ internal class CodeControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { @Test @WithMockCustomUser fun `should get latest code`() { - val latestCodeEntity = - LatestCodeEntity( - userId = TestAttributes.user.id, + val code = HashMap() + code[CodeTypeDto.NORMAL] = + Code( code = "code", language = LanguageEnum.CPP, lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val latestCodeEntity = LatestCodeEntity(userId = TestAttributes.user.id, latestCode = code) mongoTemplate.insert(latestCodeEntity) - val expectedDto = CodeDto( - code = latestCodeEntity.code, + code = latestCodeEntity.latestCode[CodeTypeDto.NORMAL]?.code.toString(), language = LanguageDto.CPP, - lastSavedAt = latestCodeEntity.lastSavedAt + lastSavedAt = latestCodeEntity.latestCode[CodeTypeDto.NORMAL]?.lastSavedAt + ?: Instant.MIN ) mockMvc.get("/user/code/latest") { contentType = MediaType.APPLICATION_JSON }.andExpect { @@ -129,17 +132,20 @@ internal class CodeControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { @Test @WithMockCustomUser fun `should update latest code`() { - val oldCodeEntity = - LatestCodeEntity( - userId = TestAttributes.user.id, - code = "#include ", + val oldCode = HashMap() + oldCode[CodeTypeDto.NORMAL] = + Code( + code = "code", language = LanguageEnum.CPP, lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val oldCodeEntity = LatestCodeEntity(userId = TestAttributes.user.id, latestCode = oldCode) mongoTemplate.insert(oldCodeEntity) - val dto = UpdateLatestCodeRequestDto(code = "import sys", language = LanguageDto.PYTHON) - + val dto = + UpdateLatestCodeRequestDto( + code = "import sys", language = LanguageDto.PYTHON, codeType = CodeTypeDto.NORMAL + ) mockMvc .post("/user/code/latest") { content = mapper.writeValueAsString(dto) @@ -148,25 +154,33 @@ internal class CodeControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { .andExpect { status { is2xxSuccessful() } } val latestCode = mongoTemplate.findAll().first() - assertThat(latestCode.code).isEqualTo(dto.code) - assertThat(latestCode.language).isEqualTo(LanguageEnum.valueOf(dto.language.name)) - assertThat(latestCode.lastSavedAt).isNotEqualTo(oldCodeEntity.lastSavedAt) + assertThat(latestCode.latestCode[CodeTypeDto.NORMAL]?.code).isEqualTo(dto.code) + assertThat(latestCode.latestCode[CodeTypeDto.NORMAL]?.language) + .isEqualTo(LanguageEnum.valueOf(dto.language.name)) + assertThat(latestCode.latestCode[CodeTypeDto.NORMAL]?.lastSavedAt) + .isNotEqualTo(oldCodeEntity.latestCode[CodeTypeDto.NORMAL]?.lastSavedAt) } @Test @WithMockCustomUser fun `should update latest code with lock`() { - val oldCodeEntity = - LatestCodeEntity( - userId = TestAttributes.user.id, - code = "#include ", + val oldCode = HashMap() + oldCode[CodeTypeDto.NORMAL] = + Code( + code = "code", language = LanguageEnum.CPP, lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val oldCodeEntity = LatestCodeEntity(userId = TestAttributes.user.id, latestCode = oldCode) mongoTemplate.insert(oldCodeEntity) val dto = - UpdateLatestCodeRequestDto(code = "import sys", language = LanguageDto.PYTHON, lock = true) + UpdateLatestCodeRequestDto( + code = "import sys", + language = LanguageDto.PYTHON, + lock = true, + codeType = CodeTypeDto.NORMAL + ) mockMvc .post("/user/code/latest") { @@ -176,13 +190,16 @@ internal class CodeControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { .andExpect { status { is2xxSuccessful() } } val latestCode = mongoTemplate.findAll().first() - assertThat(latestCode.code).isEqualTo(dto.code) - assertThat(latestCode.language).isEqualTo(LanguageEnum.valueOf(dto.language.name)) - assertThat(latestCode.lastSavedAt).isNotEqualTo(oldCodeEntity.lastSavedAt) + assertThat(latestCode.latestCode[CodeTypeDto.NORMAL]?.code).isEqualTo(dto.code) + assertThat(latestCode.latestCode[CodeTypeDto.NORMAL]?.language) + .isEqualTo(LanguageEnum.valueOf(dto.language.name)) + assertThat(latestCode.latestCode[CodeTypeDto.NORMAL]?.lastSavedAt) + .isNotEqualTo(oldCodeEntity.latestCode[CodeTypeDto.NORMAL]?.lastSavedAt) val lockedCode = mongoTemplate.findAll().first() - assertThat(lockedCode.code).isEqualTo(dto.code) - assertThat(lockedCode.language).isEqualTo(LanguageEnum.valueOf(dto.language.name)) + assertThat(lockedCode.lockedCode[CodeTypeDto.NORMAL]?.code).isEqualTo(dto.code) + assertThat(lockedCode.lockedCode[CodeTypeDto.NORMAL]?.language) + .isEqualTo(LanguageEnum.valueOf(dto.language.name)) } @AfterEach diff --git a/server/src/test/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionServiceTest.kt index 9a836b92..711fffcd 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionServiceTest.kt @@ -1,5 +1,6 @@ package delta.codecharacter.server.code.code_revision +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.CreateCodeRevisionRequestDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.server.code.LanguageEnum @@ -30,18 +31,26 @@ internal class CodeRevisionServiceTest { val createCodeRevisionRequestDto = CreateCodeRevisionRequestDto( code = "code", + codeType = CodeTypeDto.NORMAL, message = "message", language = LanguageDto.C, ) val codeRevisionEntity = mockk() - every { codeRevisionRepository.findFirstByUserIdOrderByCreatedAtDesc(userId) } returns - Optional.of(codeRevisionEntity) + every { + codeRevisionRepository.findFirstByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId, CodeTypeDto.NORMAL + ) + } returns Optional.of(codeRevisionEntity) every { codeRevisionRepository.save(any()) } returns codeRevisionEntity codeRevisionService.createCodeRevision(userId, createCodeRevisionRequestDto) - verify { codeRevisionRepository.findFirstByUserIdOrderByCreatedAtDesc(userId) } + verify { + codeRevisionRepository.findFirstByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId, CodeTypeDto.NORMAL + ) + } verify { codeRevisionRepository.save(any()) } confirmVerified(codeRevisionRepository) @@ -55,19 +64,27 @@ internal class CodeRevisionServiceTest { id = UUID.randomUUID(), userId = userId, code = "code", + codeType = CodeTypeDto.NORMAL, message = "message", language = LanguageEnum.C, parentRevision = null, createdAt = Instant.now(), ) - every { codeRevisionRepository.findAllByUserIdOrderByCreatedAtDesc(userId) } returns - listOf(codeRevisionEntity) + every { + codeRevisionRepository.findAllByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId, CodeTypeDto.NORMAL + ) + } returns listOf(codeRevisionEntity) val codeRevisionDtos = codeRevisionService.getCodeRevisions(userId) val codeRevisionDto = codeRevisionDtos.first() - verify { codeRevisionRepository.findAllByUserIdOrderByCreatedAtDesc(userId) } + verify { + codeRevisionRepository.findAllByUserIdAndCodeTypeOrderByCreatedAtDesc( + userId, CodeTypeDto.NORMAL + ) + } confirmVerified(codeRevisionRepository) assertThat(codeRevisionDtos.size).isEqualTo(1) diff --git a/server/src/test/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeServiceTest.kt index cfaec7b4..c4a4105c 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/code/latest_code/LatestCodeServiceTest.kt @@ -1,7 +1,9 @@ package delta.codecharacter.server.code.latest_code +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto +import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.config.DefaultCodeMapConfiguration import io.mockk.confirmVerified @@ -12,6 +14,7 @@ import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.Instant +import java.time.temporal.ChronoUnit import java.util.Optional import java.util.UUID @@ -29,17 +32,24 @@ internal class LatestCodeServiceTest { @Test fun `should return latest code`() { - val userId = UUID.randomUUID() - val latestCodeEntity = - LatestCodeEntity( - code = "code", language = LanguageEnum.C, userId = userId, lastSavedAt = Instant.now() + val code = HashMap() + code[CodeTypeDto.NORMAL] = + Code( + code = "code", + language = LanguageEnum.CPP, + lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val userId = UUID.randomUUID() + val latestCodeEntity = LatestCodeEntity(userId = userId, latestCode = code) every { defaultCodeMapConfiguration.defaultCode } returns "code" - every { defaultCodeMapConfiguration.defaultLanguage } returns LanguageEnum.C + every { defaultCodeMapConfiguration.defaultLanguage } returns LanguageEnum.CPP + every { defaultCodeMapConfiguration.defaultLatestCode } returns + Code(code = "code", language = LanguageEnum.CPP, lastSavedAt = Instant.MIN) + every { latestCodeRepository.findById(userId) } returns Optional.of(latestCodeEntity) - val latestCode = latestCodeService.getLatestCode(userId) + val latestCode = latestCodeService.getLatestCode(userId, CodeTypeDto.NORMAL) verify { latestCodeRepository.findById(userId) } confirmVerified(latestCodeRepository) @@ -48,18 +58,26 @@ internal class LatestCodeServiceTest { @Test fun `should update latest code`() { + val code = HashMap() + code[CodeTypeDto.NORMAL] = + Code( + code = "code", + language = LanguageEnum.CPP, + lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) + ) val userId = UUID.randomUUID() - val latestCodeEntity = - LatestCodeEntity( - code = "code", language = LanguageEnum.C, userId = userId, lastSavedAt = Instant.now() + val latestCodeEntity = LatestCodeEntity(userId = userId, latestCode = code) + val codeDto = + UpdateLatestCodeRequestDto( + code = latestCodeEntity.latestCode[CodeTypeDto.NORMAL]?.code.toString(), + language = LanguageDto.CPP ) - val codeDto = UpdateLatestCodeRequestDto(code = latestCodeEntity.code, language = LanguageDto.C) every { latestCodeRepository.save(any()) } returns latestCodeEntity - + every { latestCodeRepository.findById(userId) } returns Optional.of(latestCodeEntity) latestCodeService.updateLatestCode(userId, codeDto) - verify { latestCodeRepository.save(any()) } + verify { latestCodeRepository.findById(userId) } confirmVerified(latestCodeRepository) } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeServiceTest.kt index edfd4ccd..183151c2 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/code/locked_code/LockedCodeServiceTest.kt @@ -1,7 +1,9 @@ package delta.codecharacter.server.code.locked_code +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.dtos.UpdateLatestCodeRequestDto +import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.config.DefaultCodeMapConfiguration import io.mockk.confirmVerified @@ -11,6 +13,8 @@ import io.mockk.verify import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.temporal.ChronoUnit import java.util.Optional import java.util.UUID @@ -28,12 +32,21 @@ internal class LockedCodeServiceTest { @Test fun `should return latest code`() { + val code = HashMap() + code[CodeTypeDto.NORMAL] = + Code( + code = "code", + language = LanguageEnum.CPP, + lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) + ) val userId = UUID.randomUUID() - val lockedCodeEntity = - LockedCodeEntity(code = "code", language = LanguageEnum.C, userId = userId) + val lockedCodeEntity = LockedCodeEntity(userId = userId, lockedCode = code) every { defaultCodeMapConfiguration.defaultCode } returns "code" - every { defaultCodeMapConfiguration.defaultLanguage } returns LanguageEnum.C + every { defaultCodeMapConfiguration.defaultLanguage } returns LanguageEnum.CPP + every { defaultCodeMapConfiguration.defaultLockedCode } returns + Code(code = "code", language = LanguageEnum.CPP) + every { lockedCodeRepository.findById(userId) } returns Optional.of(lockedCodeEntity) val latestCode = lockedCodeService.getLockedCode(userId) @@ -46,14 +59,19 @@ internal class LockedCodeServiceTest { @Test fun `should update latest code`() { val userId = UUID.randomUUID() - val lockedCodeEntity = - LockedCodeEntity(code = "code", language = LanguageEnum.C, userId = userId) - val codeDto = UpdateLatestCodeRequestDto(code = lockedCodeEntity.code, language = LanguageDto.C) + val code = HashMap() + code[CodeTypeDto.NORMAL] = Code(code = "code", language = LanguageEnum.CPP) + val lockedCodeEntity = LockedCodeEntity(userId = userId, lockedCode = code) + val codeDto = + UpdateLatestCodeRequestDto( + code = lockedCodeEntity.lockedCode[CodeTypeDto.NORMAL]?.code.toString(), + language = LanguageDto.C + ) every { lockedCodeRepository.save(any()) } returns lockedCodeEntity - + every { lockedCodeRepository.findById(userId) } returns Optional.of(lockedCodeEntity) lockedCodeService.updateLockedCode(userId, codeDto) - + verify { lockedCodeRepository.findById(userId) } verify { lockedCodeRepository.save(any()) } confirmVerified(lockedCodeRepository) } diff --git a/server/src/test/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeControllerIntegrationTest.kt b/server/src/test/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeControllerIntegrationTest.kt new file mode 100644 index 00000000..2a46b13e --- /dev/null +++ b/server/src/test/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeControllerIntegrationTest.kt @@ -0,0 +1,66 @@ +package delta.codecharacter.server.daily_challenge + +import com.fasterxml.jackson.databind.ObjectMapper +import delta.codecharacter.dtos.DailyChallengeMatchRequestDto +import delta.codecharacter.server.TestAttributes +import delta.codecharacter.server.WithMockCustomUser +import delta.codecharacter.server.user.UserEntity +import delta.codecharacter.server.user.public_user.PublicUserEntity +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.dropCollection +import org.springframework.http.MediaType +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import org.springframework.test.util.ReflectionTestUtils +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post +import java.time.Instant + +@AutoConfigureMockMvc +@SpringBootTest +internal class DailyChallengeControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { + + @Autowired private lateinit var dailyChallengeService: DailyChallengeService + + @Autowired private lateinit var jackson2ObjectMapperBuilder: Jackson2ObjectMapperBuilder + private lateinit var mapper: ObjectMapper + + @Autowired private lateinit var mongoTemplate: MongoTemplate + + @BeforeEach + fun setUp() { + mapper = jackson2ObjectMapperBuilder.build() + mongoTemplate.save(TestAttributes.user) + mongoTemplate.save(TestAttributes.publicUser) + ReflectionTestUtils.setField(dailyChallengeService, "startDate", Instant.now().toString()) + } + + @Test + @WithMockCustomUser + fun `should not allow daily challenge match submission if already completed`() { + mockMvc + .post("/dc/submit") { + contentType = MediaType.APPLICATION_JSON + content = mapper.writeValueAsString(DailyChallengeMatchRequestDto(value = "[[0,0,0]]")) + } + .andExpect { + status { isBadRequest() } + content { + mapper.writeValueAsString( + mapOf("message" to "You have already completed your daily challenge") + ) + } + } + } + + @AfterEach + fun tearDown() { + mongoTemplate.dropCollection() + mongoTemplate.dropCollection() + } +} diff --git a/server/src/test/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeServiceTest.kt new file mode 100644 index 00000000..a23ab228 --- /dev/null +++ b/server/src/test/kotlin/delta/codecharacter/server/daily_challenge/DailyChallengeServiceTest.kt @@ -0,0 +1,79 @@ +package delta.codecharacter.server.daily_challenge + +import delta.codecharacter.server.TestAttributes +import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchVerdictEnum +import delta.codecharacter.server.game.GameEntity +import delta.codecharacter.server.game.GameStatusEnum +import delta.codecharacter.server.logic.daily_challenge_score.DailyChallengeScoreAlgorithm +import delta.codecharacter.server.user.public_user.PublicUserService +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.test.util.ReflectionTestUtils +import java.util.Optional +import java.util.UUID +import kotlin.collections.HashMap + +internal class DailyChallengeServiceTest { + private lateinit var dailyChallengeRepository: DailyChallengeRepository + private lateinit var publicUserService: PublicUserService + private lateinit var dailyChallengeScoreAlgorithm: DailyChallengeScoreAlgorithm + private lateinit var dailyChallengeService: DailyChallengeService + + @BeforeEach + fun setUp() { + dailyChallengeRepository = mockk(relaxed = true) + publicUserService = mockk(relaxed = true) + dailyChallengeScoreAlgorithm = mockk(relaxed = true) + dailyChallengeService = + DailyChallengeService( + dailyChallengeRepository, publicUserService, dailyChallengeScoreAlgorithm + ) + + ReflectionTestUtils.setField(dailyChallengeService, "startDate", "2023-02-15T23:00:00Z") + every { publicUserService.getPublicUser(any()) } returns + TestAttributes.publicUser.copy(dailyChallengeHistory = HashMap()) + every { dailyChallengeRepository.findByDay(any()) } returns + Optional.ofNullable(TestAttributes.dailyChallengeCode) + } + + @Test + fun `should return daily challenge for User`() { + assertThat( + dailyChallengeService.getDailyChallengeByDateForUser(UUID.randomUUID()) + .completionStatus + ) + .isEqualTo(false) + } + + @Test + fun `should return failure if game had errors`() { + val gameEntity = + GameEntity( + id = UUID.randomUUID(), + coinsUsed = 2000, + destruction = 35.0, + status = GameStatusEnum.EXECUTE_ERROR, + matchId = UUID.randomUUID() + ) + assertThat(dailyChallengeService.completeDailyChallenge(gameEntity, UUID.randomUUID())) + .isEqualTo(DailyChallengeMatchVerdictEnum.FAILURE) + } + + @Test + fun `should return success if destruction met the required criteria`() { + val gameEntity = + GameEntity( + id = UUID.randomUUID(), + coinsUsed = 2000, + destruction = 35.0, + status = GameStatusEnum.EXECUTED, + matchId = UUID.randomUUID() + ) + every { dailyChallengeRepository.save(any()) } returns mockk() + assertThat(dailyChallengeService.completeDailyChallenge(gameEntity, UUID.randomUUID())) + .isEqualTo(DailyChallengeMatchVerdictEnum.SUCCESS) + } +} diff --git a/server/src/test/kotlin/delta/codecharacter/server/game/game_log/GameLogServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/game/game_log/GameLogServiceTest.kt index 15f72c65..8804bd2f 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/game/game_log/GameLogServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/game/game_log/GameLogServiceTest.kt @@ -1,6 +1,5 @@ package delta.codecharacter.server.game.game_log -import delta.codecharacter.server.game.GameEntity import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk @@ -24,7 +23,6 @@ internal class GameLogServiceTest { @Test fun `should return game log`() { val gameId = UUID.randomUUID() - val gameEntity = mockk() val gameLogEntity = mockk() val expectedGameLog = "game log" diff --git a/server/src/test/kotlin/delta/codecharacter/server/game_map/GameMapControllerIntegrationTest.kt b/server/src/test/kotlin/delta/codecharacter/server/game_map/GameMapControllerIntegrationTest.kt index dd23abc2..b4bb106b 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/game_map/GameMapControllerIntegrationTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/game_map/GameMapControllerIntegrationTest.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import delta.codecharacter.dtos.CreateMapRevisionRequestDto import delta.codecharacter.dtos.GameMapDto import delta.codecharacter.dtos.GameMapRevisionDto +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto import delta.codecharacter.server.TestAttributes import delta.codecharacter.server.WithMockCustomUser @@ -49,7 +50,10 @@ internal class GameMapControllerIntegrationTest(@Autowired val mockMvc: MockMvc) fun `should create map revision`() { val validMap = mapper.writeValueAsString(List(64) { List(64) { 0 } }) - val dto = CreateMapRevisionRequestDto(map = validMap, message = "message") + val dto = + CreateMapRevisionRequestDto( + map = validMap, message = "message", mapImage = "", mapType = GameMapTypeDto.NORMAL + ) mockMvc .post("/user/map/revisions") { @@ -74,6 +78,8 @@ internal class GameMapControllerIntegrationTest(@Autowired val mockMvc: MockMvc) id = UUID.randomUUID(), map = "map", message = "message", + mapImage = "", + mapType = GameMapTypeDto.NORMAL, userId = TestAttributes.user.id, parentRevision = null, createdAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) @@ -99,16 +105,23 @@ internal class GameMapControllerIntegrationTest(@Autowired val mockMvc: MockMvc) @Test @WithMockCustomUser fun `should get latest map`() { - val latestMapEntity = - LatestMapEntity( - userId = TestAttributes.user.id, + val latestMap = HashMap() + latestMap[GameMapTypeDto.NORMAL] = + GameMap( map = "map", + mapImage = "base64", lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val latestMapEntity = LatestMapEntity(userId = TestAttributes.user.id, latestMap = latestMap) mongoTemplate.insert(latestMapEntity) val expectedDto = - GameMapDto(map = latestMapEntity.map, lastSavedAt = latestMapEntity.lastSavedAt) + GameMapDto( + map = latestMapEntity.latestMap[GameMapTypeDto.NORMAL]?.map.toString(), + lastSavedAt = latestMapEntity.latestMap[GameMapTypeDto.NORMAL]?.lastSavedAt + ?: Instant.MIN, + mapImage = "base64" + ) mockMvc.get("/user/map/latest") { contentType = MediaType.APPLICATION_JSON }.andExpect { status { is2xxSuccessful() } @@ -119,17 +132,20 @@ internal class GameMapControllerIntegrationTest(@Autowired val mockMvc: MockMvc) @Test @WithMockCustomUser fun `should update latest map`() { - val validMap = mapper.writeValueAsString(List(64) { List(64) { 0 } }) - val oldMapEntity = - LatestMapEntity( - userId = TestAttributes.user.id, - map = validMap, + val oldMap = HashMap() + oldMap[GameMapTypeDto.NORMAL] = + GameMap( + map = "map", + mapImage = "base64", lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val validMap = mapper.writeValueAsString(List(64) { List(64) { 0 } }) + val oldMapEntity = LatestMapEntity(userId = TestAttributes.user.id, latestMap = oldMap) mongoTemplate.insert(oldMapEntity) - val dto = UpdateLatestMapRequestDto(map = validMap) + val dto = + UpdateLatestMapRequestDto(map = validMap, mapImage = "", mapType = GameMapTypeDto.NORMAL) mockMvc .post("/user/map/latest") { @@ -139,23 +155,29 @@ internal class GameMapControllerIntegrationTest(@Autowired val mockMvc: MockMvc) .andExpect { status { is2xxSuccessful() } } val latestMap = mongoTemplate.findAll().first() - assertThat(latestMap.map).isEqualTo(dto.map) - assertThat(latestMap.lastSavedAt).isNotEqualTo(oldMapEntity.lastSavedAt) + assertThat(latestMap.latestMap[GameMapTypeDto.NORMAL]?.map).isEqualTo(dto.map) + assertThat(latestMap.latestMap[GameMapTypeDto.NORMAL]?.lastSavedAt) + .isNotEqualTo(oldMapEntity.latestMap[GameMapTypeDto.NORMAL]?.lastSavedAt) } @Test @WithMockCustomUser fun `should update latest map with lock`() { val validMap = mapper.writeValueAsString(List(64) { List(64) { 0 } }) - val oldMapEntity = - LatestMapEntity( - userId = TestAttributes.user.id, - map = validMap, + val oldMap = HashMap() + oldMap[GameMapTypeDto.NORMAL] = + GameMap( + map = "map", + mapImage = "base64", lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) ) + val oldMapEntity = LatestMapEntity(userId = TestAttributes.user.id, latestMap = oldMap) mongoTemplate.insert(oldMapEntity) - val dto = UpdateLatestMapRequestDto(map = validMap, lock = true) + val dto = + UpdateLatestMapRequestDto( + map = validMap, lock = true, mapImage = "", mapType = GameMapTypeDto.NORMAL + ) mockMvc .post("/user/map/latest") { @@ -165,11 +187,12 @@ internal class GameMapControllerIntegrationTest(@Autowired val mockMvc: MockMvc) .andExpect { status { is2xxSuccessful() } } val latestMap = mongoTemplate.findAll().first() - assertThat(latestMap.map).isEqualTo(dto.map) - assertThat(latestMap.lastSavedAt).isNotEqualTo(oldMapEntity.lastSavedAt) + assertThat(latestMap.latestMap[GameMapTypeDto.NORMAL]?.map).isEqualTo(dto.map) + assertThat(latestMap.latestMap[GameMapTypeDto.NORMAL]?.map) + .isNotEqualTo(oldMapEntity.latestMap[GameMapTypeDto.NORMAL]?.map) val lockedMap = mongoTemplate.findAll().first() - assertThat(lockedMap.map).isEqualTo(dto.map) + assertThat(lockedMap.lockedMap[GameMapTypeDto.NORMAL]?.map).isEqualTo(dto.map) } @AfterEach diff --git a/server/src/test/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapServiceTest.kt index 637638db..6f6cc91c 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/game_map/latest_map/LatestMapServiceTest.kt @@ -1,7 +1,9 @@ package delta.codecharacter.server.game_map.latest_map +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto import delta.codecharacter.server.config.DefaultCodeMapConfiguration +import delta.codecharacter.server.game_map.GameMap import delta.codecharacter.server.logic.validation.MapValidator import io.mockk.confirmVerified import io.mockk.every @@ -11,6 +13,7 @@ import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.Instant +import java.time.temporal.ChronoUnit import java.util.Optional import java.util.UUID @@ -32,10 +35,19 @@ internal class LatestMapServiceTest { @Test fun `should return latest map`() { val userId = UUID.randomUUID() - val latestMapEntity = - LatestMapEntity(map = "[[0]]", userId = userId, lastSavedAt = Instant.now()) + val map = HashMap() + map[GameMapTypeDto.NORMAL] = + GameMap( + map = "map", + mapImage = "base64", + lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) + ) + val latestMapEntity = LatestMapEntity(userId = userId, latestMap = map) every { defaultCodeMapConfiguration.defaultMap } returns "[[0]]" + every { defaultCodeMapConfiguration.defaultMapImage } returns "base64" + every { defaultCodeMapConfiguration.defaultLatestGameMap } returns + GameMap(mapImage = "base64", map = "[[0]]", lastSavedAt = Instant.MIN) every { latestMapRepository.findById(userId) } returns Optional.of(latestMapEntity) val latestMap = latestMapService.getLatestMap(userId) @@ -48,17 +60,30 @@ internal class LatestMapServiceTest { @Test fun `should update latest map`() { val userId = UUID.randomUUID() - val latestMapEntity = - LatestMapEntity(map = "[[0]]", userId = userId, lastSavedAt = Instant.now()) - val mapDto = UpdateLatestMapRequestDto(map = latestMapEntity.map) + val oldlatestMap = HashMap() + oldlatestMap[GameMapTypeDto.NORMAL] = + GameMap( + map = "map", + mapImage = "base64", + lastSavedAt = Instant.now().truncatedTo(ChronoUnit.MILLIS) + ) + val latestMapEntity = LatestMapEntity(userId = userId, latestMap = oldlatestMap) + val mapDto = + UpdateLatestMapRequestDto( + map = latestMapEntity.latestMap[GameMapTypeDto.NORMAL]?.map.toString(), mapImage = "" + ) every { latestMapRepository.save(any()) } returns latestMapEntity - every { mapValidator.validateMap(latestMapEntity.map) } returns Unit - + every { + mapValidator.validateMap(latestMapEntity.latestMap[GameMapTypeDto.NORMAL]?.map.toString()) + } returns Unit + every { latestMapRepository.findById(userId) } returns Optional.of(latestMapEntity) latestMapService.updateLatestMap(userId, mapDto) - + verify { latestMapRepository.findById(userId) } verify { latestMapRepository.save(any()) } - verify { mapValidator.validateMap(latestMapEntity.map) } + verify { + mapValidator.validateMap(latestMapEntity.latestMap[GameMapTypeDto.NORMAL]?.map.toString()) + } confirmVerified(latestMapRepository, mapValidator) } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapServiceTest.kt index 8b3fbfbb..1c09f1e1 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/game_map/locked_map/LockedMapServiceTest.kt @@ -1,7 +1,9 @@ package delta.codecharacter.server.game_map.locked_map +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.UpdateLatestMapRequestDto import delta.codecharacter.server.config.DefaultCodeMapConfiguration +import delta.codecharacter.server.game_map.GameMap import delta.codecharacter.server.logic.validation.MapValidator import io.mockk.confirmVerified import io.mockk.every @@ -31,9 +33,15 @@ internal class LockedMapServiceTest { @Test fun `should return latest map`() { val userId = UUID.randomUUID() - val lockedMapEntity = LockedMapEntity(map = "map", userId = userId) + val lockedMap = HashMap() + lockedMap[GameMapTypeDto.NORMAL] = GameMap(map = "map", mapImage = "base64") + val lockedMapEntity = LockedMapEntity(userId = userId, lockedMap = lockedMap) every { defaultCodeMapConfiguration.defaultMap } returns "[[0]]" + every { defaultCodeMapConfiguration.defaultMap } returns "base64" + every { defaultCodeMapConfiguration.defaultLockedGameMap } returns + GameMap(mapImage = "base64", map = "[[0]]") + every { lockedMapRepository.findById(userId) } returns Optional.of(lockedMapEntity) val latestMap = lockedMapService.getLockedMap(userId) @@ -46,16 +54,25 @@ internal class LockedMapServiceTest { @Test fun `should update latest map`() { val userId = UUID.randomUUID() - val lockedMapEntity = LockedMapEntity(map = "map", userId = userId) - val mapDto = UpdateLatestMapRequestDto(map = lockedMapEntity.map) + val lockedMap = HashMap() + lockedMap[GameMapTypeDto.NORMAL] = GameMap(map = "map", mapImage = "base64") + val lockedMapEntity = LockedMapEntity(userId = userId, lockedMap = lockedMap) + val mapDto = + UpdateLatestMapRequestDto( + map = lockedMapEntity.lockedMap[GameMapTypeDto.NORMAL]?.map.toString(), mapImage = "" + ) every { lockedMapRepository.save(any()) } returns lockedMapEntity - every { mapValidator.validateMap(lockedMapEntity.map) } returns Unit - + every { + mapValidator.validateMap(lockedMapEntity.lockedMap[GameMapTypeDto.NORMAL]?.map.toString()) + } returns Unit + every { lockedMapRepository.findById(userId) } returns Optional.of(lockedMapEntity) lockedMapService.updateLockedMap(userId, mapDto) - + verify { lockedMapRepository.findById(userId) } verify { lockedMapRepository.save(any()) } - verify { mapValidator.validateMap(lockedMapEntity.map) } + verify { + mapValidator.validateMap(lockedMapEntity.lockedMap[GameMapTypeDto.NORMAL]?.map.toString()) + } confirmVerified(lockedMapRepository, mapValidator) } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionServiceTest.kt index 196525a1..5904a1b4 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/game_map/map_revision/MapRevisionServiceTest.kt @@ -1,6 +1,7 @@ package delta.codecharacter.server.game_map.map_revision import delta.codecharacter.dtos.CreateMapRevisionRequestDto +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.server.logic.validation.MapValidator import io.mockk.confirmVerified import io.mockk.every @@ -31,18 +32,27 @@ internal class MapRevisionServiceTest { val createMapRevisionRequestDto = CreateMapRevisionRequestDto( map = "map", + mapType = GameMapTypeDto.NORMAL, + mapImage = "", message = "message", ) val mapRevisionEntity = mockk() - every { mapRevisionRepository.findFirstByUserIdOrderByCreatedAtDesc(userId) } returns - Optional.of(mapRevisionEntity) + every { + mapRevisionRepository.findFirstByUserIdAndMapTypeOrderByCreatedAtDesc( + userId, GameMapTypeDto.NORMAL + ) + } returns Optional.of(mapRevisionEntity) every { mapRevisionRepository.save(any()) } returns mapRevisionEntity every { mapValidator.validateMap(createMapRevisionRequestDto.map) } returns Unit mapRevisionService.createMapRevision(userId, createMapRevisionRequestDto) - verify { mapRevisionRepository.findFirstByUserIdOrderByCreatedAtDesc(userId) } + verify { + mapRevisionRepository.findFirstByUserIdAndMapTypeOrderByCreatedAtDesc( + userId, GameMapTypeDto.NORMAL + ) + } verify { mapValidator.validateMap(createMapRevisionRequestDto.map) } verify { mapRevisionRepository.save(any()) } @@ -57,18 +67,27 @@ internal class MapRevisionServiceTest { id = UUID.randomUUID(), userId = userId, map = "map", + mapType = GameMapTypeDto.NORMAL, + mapImage = "", message = "message", parentRevision = null, createdAt = Instant.now(), ) - every { mapRevisionRepository.findAllByUserIdOrderByCreatedAtDesc(userId) } returns - listOf(mapRevisionEntity) + every { + mapRevisionRepository.findAllByUserIdAndMapTypeOrderByCreatedAtDesc( + userId, GameMapTypeDto.NORMAL + ) + } returns listOf(mapRevisionEntity) val mapRevisionDtos = mapRevisionService.getMapRevisions(userId) val mapRevisionDto = mapRevisionDtos.first() - verify { mapRevisionRepository.findAllByUserIdOrderByCreatedAtDesc(userId) } + verify { + mapRevisionRepository.findAllByUserIdAndMapTypeOrderByCreatedAtDesc( + userId, GameMapTypeDto.NORMAL + ) + } confirmVerified(mapRevisionRepository) assertThat(mapRevisionDtos.size).isEqualTo(1) diff --git a/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardControllerIntegrationTest.kt b/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardControllerIntegrationTest.kt new file mode 100644 index 00000000..331328c1 --- /dev/null +++ b/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardControllerIntegrationTest.kt @@ -0,0 +1,125 @@ +package delta.codecharacter.server.leaderboard + +import com.fasterxml.jackson.databind.ObjectMapper +import delta.codecharacter.dtos.LeaderboardEntryDto +import delta.codecharacter.dtos.PublicUserDto +import delta.codecharacter.dtos.TierTypeDto +import delta.codecharacter.dtos.UserStatsDto +import delta.codecharacter.server.TestAttributes +import delta.codecharacter.server.WithMockCustomUser +import delta.codecharacter.server.user.public_user.PublicUserEntity +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.dropCollection +import org.springframework.http.MediaType +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import java.math.BigDecimal + +@AutoConfigureMockMvc +@SpringBootTest +internal class LeaderboardControllerIntegrationTest(@Autowired val mockMvc: MockMvc) { + + @Autowired private lateinit var jackson2ObjectMapperBuilder: Jackson2ObjectMapperBuilder + private lateinit var mapper: ObjectMapper + + @Autowired private lateinit var mongoTemplate: MongoTemplate + + @BeforeEach + fun setUp() { + mapper = jackson2ObjectMapperBuilder.build() + mongoTemplate.save(TestAttributes.publicUser) + } + + @Test + @WithMockCustomUser + fun `should get leaderboard when tier type is either TIER_PRACTICE , TIER1 and TIER2`() { + val testUser = TestAttributes.publicUser.copy(tier = TierTypeDto.TIER1) + mongoTemplate.save(testUser) + val expectedDto = + listOf( + LeaderboardEntryDto( + user = + PublicUserDto( + username = testUser.username, + name = testUser.name, + tier = TierTypeDto.valueOf(testUser.tier.name), + country = testUser.country, + college = testUser.college, + avatarId = testUser.avatarId, + ), + stats = + UserStatsDto( + rating = BigDecimal(testUser.rating), + wins = testUser.wins, + losses = testUser.losses, + ties = testUser.ties + ), + ) + ) + mockMvc + .get("/leaderboard?page=0&size=2&tier=TIER1") { contentType = MediaType.APPLICATION_JSON } + .andExpect { + status { is2xxSuccessful() } + content { contentType(MediaType.APPLICATION_JSON) } + content { json(mapper.writeValueAsString(expectedDto)) } + } + } + + @Test + @WithMockCustomUser + fun `should get empty array for invalid tier type when leaderboard contains tier of different type`() { + val testUser = TestAttributes.publicUser.copy(tier = TierTypeDto.TIER1) + mongoTemplate.save(testUser) + val expectedDto = listOf() + mockMvc + .get("/leaderboard?page=0&size=2&tier=TIER2") { contentType = MediaType.APPLICATION_JSON } + .andExpect { + status { is2xxSuccessful() } + content { contentType(MediaType.APPLICATION_JSON) } + content { json(mapper.writeValueAsString(expectedDto)) } + } + } + + @Test + @WithMockCustomUser + fun `should return all entries when tier not found`() { + val testUser = TestAttributes.publicUser + val expectedDto = + listOf( + LeaderboardEntryDto( + user = + PublicUserDto( + username = testUser.username, + name = testUser.name, + tier = TierTypeDto.valueOf(testUser.tier.name), + country = testUser.country, + college = testUser.college, + avatarId = testUser.avatarId, + ), + stats = + UserStatsDto( + rating = BigDecimal(testUser.rating), + wins = testUser.wins, + losses = testUser.losses, + ties = testUser.ties + ), + ) + ) + mockMvc.get("/leaderboard") { contentType = MediaType.APPLICATION_JSON }.andExpect { + status { is2xxSuccessful() } + content { contentType(MediaType.APPLICATION_JSON) } + content { json(mapper.writeValueAsString(expectedDto)) } + } + } + @AfterEach + fun tearDown() { + mongoTemplate.dropCollection() + } +} diff --git a/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt b/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt new file mode 100644 index 00000000..a81daaae --- /dev/null +++ b/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt @@ -0,0 +1,139 @@ +package delta.codecharacter.server.leaderboard + +import delta.codecharacter.dtos.TierTypeDto +import delta.codecharacter.server.TestAttributes +import delta.codecharacter.server.user.public_user.DailyChallengeHistory +import delta.codecharacter.server.user.public_user.PublicUserEntity +import delta.codecharacter.server.user.public_user.PublicUserRepository +import delta.codecharacter.server.user.public_user.PublicUserService +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.UUID + +internal class LeaderboardTest { + + private lateinit var publicUserRepository: PublicUserRepository + private lateinit var publicUserService: PublicUserService + private lateinit var publicUserEntity: PublicUserEntity + + private var user1 = + PublicUserEntity( + userId = UUID.randomUUID(), + username = "testUser1", + name = "test user", + country = "In", + college = "college", + avatarId = 1, + tier = TierTypeDto.TIER1, + tutorialLevel = 1, + rating = 2000.0, + wins = 0, + losses = 0, + ties = 0, + isActivated = true, + score = 0.0, + dailyChallengeHistory = + hashMapOf(0 to DailyChallengeHistory(0.0, TestAttributes.dailyChallengeCode)), + ) + private var user2 = + PublicUserEntity( + userId = UUID.randomUUID(), + username = "testUser2", + name = "test user", + country = "In", + college = "college", + avatarId = 1, + tier = TierTypeDto.TIER1, + tutorialLevel = 1, + rating = 1800.0, + wins = 0, + losses = 0, + ties = 0, + isActivated = true, + score = 0.0, + dailyChallengeHistory = + hashMapOf(0 to DailyChallengeHistory(0.0, TestAttributes.dailyChallengeCode)), + ) + private var user3 = + PublicUserEntity( + userId = UUID.randomUUID(), + username = "testUser3", + name = "test user", + country = "In", + college = "college", + avatarId = 1, + tier = TierTypeDto.TIER2, + tutorialLevel = 1, + rating = 1600.0, + wins = 0, + losses = 0, + ties = 0, + isActivated = true, + score = 0.0, + dailyChallengeHistory = + hashMapOf(0 to DailyChallengeHistory(0.0, TestAttributes.dailyChallengeCode)), + ) + private var user4 = + PublicUserEntity( + userId = UUID.randomUUID(), + username = "testUser4", + name = "test user", + country = "In", + college = "college", + avatarId = 1, + tier = TierTypeDto.TIER2, + tutorialLevel = 1, + rating = 1500.0, + wins = 0, + losses = 0, + ties = 0, + isActivated = true, + score = 0.0, + dailyChallengeHistory = + hashMapOf(0 to DailyChallengeHistory(0.0, TestAttributes.dailyChallengeCode)), + ) + + @BeforeEach + fun setUp() { + publicUserRepository = mockk(relaxed = true) + publicUserService = PublicUserService(publicUserRepository) + publicUserEntity = mockk(relaxed = true) + } + @Test + fun `should get leaderboard by tiers`() { + + every { publicUserRepository.findAllByTier(TierTypeDto.TIER1, any()) } returns + listOf(user1, user2) + every { publicUserRepository.findAllByTier(TierTypeDto.TIER2, any()) } returns + listOf(user3, user4) + + publicUserService.getLeaderboard(0, 10, TierTypeDto.TIER1).forEach { user -> + assertThat(user.user.tier).isEqualTo(TierTypeDto.TIER1) + } + publicUserService.getLeaderboard(0, 10, TierTypeDto.TIER2).forEach { user -> + assertThat(user.user.tier).isEqualTo(TierTypeDto.TIER2) + } + + verify { publicUserRepository.findAllByTier(TierTypeDto.TIER1, any()) } + verify { publicUserRepository.findAllByTier(TierTypeDto.TIER2, any()) } + confirmVerified(publicUserRepository) + } + + @Test + fun `should promote and demote player tiers`() { + every { publicUserRepository.findAllByTier(TierTypeDto.TIER1, any()) } returns + listOf(user2, user1) + every { publicUserRepository.findAllByTier(TierTypeDto.TIER2, any()) } returns + listOf(user3, user4) + every { publicUserRepository.save(any()) } returns publicUserEntity + publicUserService.promoteTiers() + verify { publicUserRepository.save(any()) } + verify { publicUserRepository.findAllByTier(any(), any()) } + confirmVerified(publicUserRepository) + } +} diff --git a/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt index 63aba137..b57d784c 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt @@ -1,7 +1,10 @@ package delta.codecharacter.server.match +import delta.codecharacter.dtos.ChallengeTypeDto import delta.codecharacter.dtos.CodeRevisionDto import delta.codecharacter.dtos.CreateMatchRequestDto +import delta.codecharacter.dtos.DailyChallengeGetRequestDto +import delta.codecharacter.dtos.DailyChallengeMatchRequestDto import delta.codecharacter.dtos.GameMapRevisionDto import delta.codecharacter.dtos.LanguageDto import delta.codecharacter.dtos.MatchModeDto @@ -10,12 +13,15 @@ import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.code.code_revision.CodeRevisionService import delta.codecharacter.server.code.latest_code.LatestCodeService import delta.codecharacter.server.code.locked_code.LockedCodeService +import delta.codecharacter.server.daily_challenge.DailyChallengeService +import delta.codecharacter.server.daily_challenge.match.DailyChallengeMatchRepository import delta.codecharacter.server.exception.CustomException import delta.codecharacter.server.game.GameEntity import delta.codecharacter.server.game.GameService import delta.codecharacter.server.game_map.latest_map.LatestMapService import delta.codecharacter.server.game_map.locked_map.LockedMapService import delta.codecharacter.server.game_map.map_revision.MapRevisionService +import delta.codecharacter.server.logic.validation.MapValidator import delta.codecharacter.server.logic.verdict.VerdictAlgorithm import delta.codecharacter.server.notifications.NotificationService import delta.codecharacter.server.user.public_user.PublicUserService @@ -48,10 +54,13 @@ internal class MatchServiceTest { private lateinit var verdictAlgorithm: VerdictAlgorithm private lateinit var ratingHistoryService: RatingHistoryService private lateinit var notificationService: NotificationService + private lateinit var dailyChallengeService: DailyChallengeService + private lateinit var dailyChallengeMatchRepository: DailyChallengeMatchRepository private lateinit var jackson2ObjectMapperBuilder: Jackson2ObjectMapperBuilder private lateinit var simpMessagingTemplate: SimpMessagingTemplate - + private lateinit var mapValidator: MapValidator private lateinit var matchService: MatchService + private lateinit var autoMatchRepository: AutoMatchRepository @BeforeEach fun setUp() { @@ -67,8 +76,12 @@ internal class MatchServiceTest { verdictAlgorithm = mockk(relaxed = true) ratingHistoryService = mockk(relaxed = true) notificationService = mockk(relaxed = true) + dailyChallengeService = mockk(relaxed = true) + dailyChallengeMatchRepository = mockk(relaxed = true) jackson2ObjectMapperBuilder = Jackson2ObjectMapperBuilder() simpMessagingTemplate = mockk(relaxed = true) + mapValidator = mockk(relaxed = true) + autoMatchRepository = mockk(relaxed = true) matchService = MatchService( @@ -84,8 +97,12 @@ internal class MatchServiceTest { verdictAlgorithm, ratingHistoryService, notificationService, + dailyChallengeService, + dailyChallengeMatchRepository, jackson2ObjectMapperBuilder, - simpMessagingTemplate + simpMessagingTemplate, + mapValidator, + autoMatchRepository ) } @@ -315,4 +332,58 @@ internal class MatchServiceTest { codeRevisionService, mapRevisionService, gameService, matchRepository, gameService ) } + + @Test + fun `should create dailyChallenge match`() { + val userId = UUID.randomUUID() + val dailyChallengeForUser = + DailyChallengeGetRequestDto( + challName = "challenge-name", + chall = TestAttributes.dailyChallengeCode.chall, + challType = ChallengeTypeDto.CODE, + description = "description", + completionStatus = false + ) + val matchRequest = DailyChallengeMatchRequestDto(value = "[[0,0,0]]") + every { dailyChallengeService.getDailyChallengeByDateForUser(any()) } returns + dailyChallengeForUser + every { dailyChallengeMatchRepository.save(any()) } returns mockk() + every { publicUserService.getPublicUser(any()) } returns TestAttributes.publicUser + every { gameService.createGame(any()) } returns mockk() + every { dailyChallengeService.getDailyChallengeByDate() } returns mockk() + every { + gameService.sendGameRequest( + any(), dailyChallengeForUser.chall.cpp.toString(), LanguageEnum.CPP, matchRequest.value + ) + } returns Unit + + matchService.createDCMatch(userId, matchRequest) + + verify { + dailyChallengeMatchRepository.save(any()) + gameService.createGame(any()) + gameService.sendGameRequest( + any(), dailyChallengeForUser.chall.cpp.toString(), LanguageEnum.CPP, matchRequest.value + ) + } + confirmVerified(dailyChallengeMatchRepository, gameService) + } + + @Test + @Throws(CustomException::class) + fun `should throw error if daily challenge is already completed`() { + val dailyChallengeForUser = + DailyChallengeGetRequestDto( + challName = "challenge-name", + chall = TestAttributes.dailyChallengeCode.chall, + challType = ChallengeTypeDto.CODE, + description = "description", + completionStatus = true + ) + every { dailyChallengeService.getDailyChallengeByDateForUser(any()) } returns + dailyChallengeForUser + val exception = assertThrows { matchService.createDCMatch(mockk(), mockk()) } + assertThat(exception.status).isEqualTo(HttpStatus.BAD_REQUEST) + assertThat(exception.message).isEqualTo("You have already completed your daily challenge") + } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/match/RabbitIntegrationTest.kt b/server/src/test/kotlin/delta/codecharacter/server/match/RabbitIntegrationTest.kt index cb8e5397..96c58a47 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/match/RabbitIntegrationTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/match/RabbitIntegrationTest.kt @@ -1,10 +1,13 @@ package delta.codecharacter.server.match import com.fasterxml.jackson.databind.ObjectMapper +import delta.codecharacter.dtos.CodeTypeDto import delta.codecharacter.dtos.CreateMatchRequestDto +import delta.codecharacter.dtos.GameMapTypeDto import delta.codecharacter.dtos.MatchModeDto import delta.codecharacter.server.TestAttributes import delta.codecharacter.server.WithMockCustomUser +import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.code.code_revision.CodeRevisionEntity import delta.codecharacter.server.code.locked_code.LockedCodeEntity @@ -14,6 +17,7 @@ import delta.codecharacter.server.game.game_log.GameLogEntity import delta.codecharacter.server.game.queue.entities.GameRequestEntity import delta.codecharacter.server.game.queue.entities.GameResultEntity import delta.codecharacter.server.game.queue.entities.GameStatusUpdateEntity +import delta.codecharacter.server.game_map.GameMap import delta.codecharacter.server.game_map.locked_map.LockedMapEntity import delta.codecharacter.server.game_map.map_revision.MapRevisionEntity import delta.codecharacter.server.user.UserEntity @@ -76,6 +80,7 @@ internal class RabbitIntegrationTest(@Autowired val mockMvc: MockMvc) { id = UUID.randomUUID(), userId = TestAttributes.user.id, code = "code", + codeType = CodeTypeDto.NORMAL, message = "message", language = LanguageEnum.PYTHON, createdAt = Instant.now(), @@ -89,6 +94,8 @@ internal class RabbitIntegrationTest(@Autowired val mockMvc: MockMvc) { id = UUID.randomUUID(), userId = TestAttributes.user.id, map = "map", + mapType = GameMapTypeDto.NORMAL, + mapImage = "", message = "message", createdAt = Instant.now(), parentRevision = null @@ -135,6 +142,11 @@ internal class RabbitIntegrationTest(@Autowired val mockMvc: MockMvc) { @Test @WithMockCustomUser fun `should create match with two games for manual mode`() { + val lockedUserCode = HashMap() + lockedUserCode[CodeTypeDto.NORMAL] = Code(code = "user-code", language = LanguageEnum.PYTHON) + + val lockedUserMap = HashMap() + lockedUserMap[GameMapTypeDto.NORMAL] = GameMap(map = "user-map", mapImage = "base64") val opponentUser = mongoTemplate.save( TestAttributes.user.copy(id = UUID.randomUUID(), email = "opponent@test.com") @@ -148,35 +160,24 @@ internal class RabbitIntegrationTest(@Autowired val mockMvc: MockMvc) { val userLockedCode = mongoTemplate.save( - LockedCodeEntity( - userId = TestAttributes.user.id, - code = "user-code", - language = LanguageEnum.PYTHON, - ) + LockedCodeEntity(userId = TestAttributes.user.id, lockedCode = lockedUserCode) ) val userLockedMap = mongoTemplate.save( - LockedMapEntity( - userId = TestAttributes.user.id, - map = "user-map", - ) + LockedMapEntity(userId = TestAttributes.user.id, lockedMap = lockedUserMap) ) + val lockedOpponentCode = HashMap() + lockedUserCode[CodeTypeDto.NORMAL] = + Code(code = "opponent-code", language = LanguageEnum.PYTHON) + val lockedOpponentMap = HashMap() + lockedUserMap[GameMapTypeDto.NORMAL] = GameMap(map = "opponent-map", mapImage = "base64") val opponentLockedCode = mongoTemplate.save( - LockedCodeEntity( - userId = opponentUser.id, - code = "opponent-code", - language = LanguageEnum.PYTHON, - ) + LockedCodeEntity(userId = opponentUser.id, lockedCode = lockedOpponentCode) ) val opponentLockedMap = - mongoTemplate.save( - LockedMapEntity( - userId = opponentUser.id, - map = "opponent-map", - ) - ) + mongoTemplate.save(LockedMapEntity(userId = opponentUser.id, lockedMap = lockedOpponentMap)) val createMatchRequestDto = CreateMatchRequestDto( @@ -210,22 +211,35 @@ internal class RabbitIntegrationTest(@Autowired val mockMvc: MockMvc) { assertThat(match.games.last()).isEqualTo(game2) val gameRequestEntityString1 = - this.rabbitTemplate.receiveAndConvert("gameRequestQueue") as String + this.rabbitTemplate.receiveAndConvert("gameRequestQueue") as String? val gameRequestEntity1 = mapper.readValue(gameRequestEntityString1, GameRequestEntity::class.java) - assertThat(gameRequestEntity1.gameId).isEqualTo(game1.id) - assertThat(gameRequestEntity1.map).isEqualTo(opponentLockedMap.map) - assertThat(gameRequestEntity1.sourceCode).isEqualTo(userLockedCode.code) - assertThat(gameRequestEntity1.language).isEqualTo(userLockedCode.language) - + if (opponentLockedMap.lockedMap[GameMapTypeDto.NORMAL] != null && + userLockedCode.lockedCode[CodeTypeDto.NORMAL] != null + ) { + assertThat(gameRequestEntity1.gameId).isEqualTo(game1.id) + assertThat(gameRequestEntity1.map) + .isEqualTo(opponentLockedMap.lockedMap[GameMapTypeDto.NORMAL]?.map.toString()) + assertThat(gameRequestEntity1.sourceCode) + .isEqualTo(userLockedCode.lockedCode[CodeTypeDto.NORMAL]?.code) + assertThat(gameRequestEntity1.language) + .isEqualTo(userLockedCode.lockedCode[CodeTypeDto.NORMAL]?.language) + } val gameRequestEntityString2 = - this.rabbitTemplate.receiveAndConvert("gameRequestQueue") as String + this.rabbitTemplate.receiveAndConvert("gameRequestQueue") as String? val gameRequestEntity2 = mapper.readValue(gameRequestEntityString2, GameRequestEntity::class.java) - assertThat(gameRequestEntity2.gameId).isEqualTo(game2.id) - assertThat(gameRequestEntity2.map).isEqualTo(userLockedMap.map) - assertThat(gameRequestEntity2.sourceCode).isEqualTo(opponentLockedCode.code) - assertThat(gameRequestEntity2.language).isEqualTo(opponentLockedCode.language) + if (userLockedMap.lockedMap[GameMapTypeDto.NORMAL] != null && + opponentLockedCode.lockedCode[CodeTypeDto.NORMAL] != null + ) { + assertThat(gameRequestEntity2.gameId).isEqualTo(game2.id) + assertThat(gameRequestEntity2.map) + .isEqualTo(userLockedMap.lockedMap[GameMapTypeDto.NORMAL]?.map) + assertThat(gameRequestEntity2.sourceCode) + .isEqualTo(opponentLockedCode.lockedCode[CodeTypeDto.NORMAL]?.code) + assertThat(gameRequestEntity2.language) + .isEqualTo(opponentLockedCode.lockedCode[CodeTypeDto.NORMAL]?.language) + } } @Test diff --git a/server/src/test/kotlin/delta/codecharacter/server/notifications/NotificationIntegrationTest.kt b/server/src/test/kotlin/delta/codecharacter/server/notifications/NotificationIntegrationTest.kt index dc21d6ca..f84bf8f8 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/notifications/NotificationIntegrationTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/notifications/NotificationIntegrationTest.kt @@ -50,7 +50,7 @@ class NotificationIntegrationTest(@Autowired val mockMvc: MockMvc) { stompSession = stompClient - .connect("ws://localhost:$port/ws", object : StompSessionHandlerAdapter() {}) + .connectAsync("ws://localhost:$port/ws", object : StompSessionHandlerAdapter() {}) .get() }