diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt index 019542c..48a3bc5 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt @@ -28,6 +28,12 @@ data class UserDetails( val stolen_replays: Int = 0 ) +data class UserDetailsExtended( + val userDetails: UserDetails, + val is_banned: Boolean = false, + val approximate_ban_date: OffsetDateTime? = null +) + @Serializable @AllowCacheSerialization data class Statistics( diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserDetailsController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserDetailsController.kt index d763b8c..620fe98 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserDetailsController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserDetailsController.kt @@ -26,6 +26,8 @@ class UserDetailsController( val suspicious_scores: List, val similar_replays: List, val total_scores: Int = 0, + val is_banned: Boolean, + val approximate_ban_date: String?, ) data class UserQueueRequest( @@ -56,18 +58,22 @@ class UserDetailsController( @PostMapping("user-details") fun getUserDetails(@RequestBody request: UserDetailsRequest): ResponseEntity { - val userDetails = this.userService.getUserDetails(username = request.userId) + val userDetailsExtended = this.userService.getUserDetails(username = request.userId) ?: return ResponseEntity.notFound().build() + val userId = userDetailsExtended.userDetails.user_id + var suspiciousScoresCondition = this.scoreService.getDefaultCondition() - suspiciousScoresCondition = suspiciousScoresCondition.and(SCORES.USER_ID.eq(userDetails.user_id)) + suspiciousScoresCondition = suspiciousScoresCondition.and(SCORES.USER_ID.eq(userId)) val response = UserDetailsResponse( - user_details = userDetails, - queue_details = this.userQueueService.getUserQueueDetails(userDetails.user_id), + user_details = userDetailsExtended.userDetails, + queue_details = this.userQueueService.getUserQueueDetails(userId), suspicious_scores = this.scoreService.getSuspiciousScores(suspiciousScoresCondition), - similar_replays = this.scoreService.getSimilarReplaysForUserId(userDetails.user_id), - total_scores = this.userService.getTotalUserScores(userDetails.user_id) + similar_replays = this.scoreService.getSimilarReplaysForUserId(userId), + total_scores = this.userService.getTotalUserScores(userId), + is_banned = userDetailsExtended.is_banned, + approximate_ban_date = userDetailsExtended.approximate_ban_date?.toString() ) return ResponseEntity.ok(response) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserService.kt index 006653c..75c7ce6 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserService.kt @@ -1,19 +1,16 @@ package com.nisemoe.nise.database -import com.nisemoe.generated.tables.records.UpdateUserQueueRecord import com.nisemoe.generated.tables.records.UsersRecord import com.nisemoe.generated.tables.references.SCORES -import com.nisemoe.generated.tables.references.UPDATE_USER_QUEUE import com.nisemoe.generated.tables.references.USERS import com.nisemoe.nise.Format import com.nisemoe.nise.UserDetails -import com.nisemoe.nise.UserQueueDetails +import com.nisemoe.nise.UserDetailsExtended import com.nisemoe.nise.osu.OsuApi import com.nisemoe.nise.osu.OsuApiModels import org.jooq.DSLContext import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import java.time.LocalDateTime import java.time.OffsetDateTime import java.time.ZoneOffset @@ -74,13 +71,13 @@ class UserService( return this.mapUserToDatabase(apiUser) } - fun getUserDetails(username: String): UserDetails? { + fun getUserDetails(username: String): UserDetailsExtended? { val user = dslContext.selectFrom(USERS) .where(USERS.USERNAME.equalIgnoreCase(username.lowercase())) .fetchOneInto(UsersRecord::class.java) if (user != null) { - return UserDetails( + val userDetails = UserDetails( user.userId!!, user.username!!, user.rank, @@ -91,6 +88,11 @@ class UserService( user.countryRank, user.playcount ) + return UserDetailsExtended( + userDetails = userDetails, + is_banned = user.isBanned ?: false, + approximate_ban_date = user.approxBanDate + ) } // The database does NOT have the user; we will now use the osu!api @@ -100,7 +102,9 @@ class UserService( // Persist to database insertApiUser(apiUser) - return this.mapUserToDatabase(apiUser) + return UserDetailsExtended( + userDetails = this.mapUserToDatabase(apiUser) + ) } fun insertApiUser(apiUser: OsuApiModels.UserExtended) { diff --git a/nise-frontend/src/app/api/api.component.html b/nise-frontend/src/app/api/api.component.html index 4e5c8f8..941faa1 100644 --- a/nise-frontend/src/app/api/api.component.html +++ b/nise-frontend/src/app/api/api.component.html @@ -4,13 +4,19 @@

if you'd like to retrieve data from our database, you are invited to use the same endpoints meant for the frontend but in a programmatic way. currently there's no rate limits.

+
+

+ if you have any issues related to CloudFlare, let me know on discord. currently all api requests are routed via CF so it's possible it might block certain IPs. +

+

as of today, you MUST pass the following parameters:

    -
  • The X-NISE-API header with a value of 20240218 in every request.
  • +
  • The X-NISE-API header with a value of 20240218 in every request. This should prevent random shit from breaking in the future (if you keep passing it)
  • The Accept header with a value of application/json (if you want JSON instead of XML)

## scores search

score search is based on predicates. a predicate is a list of specifications/conditions to match results. predicates can be combined with operators such as AND and OR

+

COMING SOON; the route exists but its cancer for api users to form the requests.

## get single score

if you have a replay_id, you can retrieve all the information 'bout that score.

@@ -19,7 +25,110 @@
  • METHOD: GET
  • Example: - curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439 +
    + + curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439 + + +

    ## get single replay (data)

    +

    if you have a replay_id, you can retrieve the data that is used for the replay viewer (replay.nise.moe) +

    it contains the beatmap data, the full replay data, and all calculated judgements (by circleguard)

    +
      +
    • ENDPOINT: /api/score/{replay_id}/replay
    • +
    • METHOD: GET
    • +
    + Example: +
    + + curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439/replay + + +

    ## get single replay (.osr)

    +

    if you have a replay_id, you can retrieve the emulated .osr replay file.

    +

    wtf is an emulated .osr replay file? 🡒 the osu!api does NOT return a "real" replay, so we fill the rest of the details.

    +

    COMING SOON

    + +

    ## get user details

    +

    if you have an user_id, you can retrieve everything we know 'bout that user. +

    >> user_id is the username. sry.

    +
      +
    • ENDPOINT: /api/user-details
    • +
    • METHOD: POST
    • +
    • POST FIELDS: user_id: str (*required)
    • +
    • POST FORMAT: JSON only
    • +
    + Example: +
    + + curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"userId": "degenerate"}' https://nise.moe/api/user-details + + +

    ## get suspicious scores

    +

    returns a list of suspicious scores (e.g. cvUR <= 25, basically /sus/)

    +

    it is not live, but rather updates every N minutes depending on server load (usually ~10 minutes)

    +
      +
    • ENDPOINT: /api/suspicious-scores
    • +
    • METHOD: GET
    • +
    + Example: +
    + + curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/suspicious-scores + + +

    ## get stolen replays

    +

    returns a list of all replay pairs with < 10 similarity ratio (basically /nu/)

    +

    it is not live, but rather updates every N minutes depending on server load (usually ~10 minutes)

    +
      +
    • ENDPOINT: /api/similar-replays
    • +
    • METHOD: GET
    • +
    + Example: +
    + + curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/similar-replays + + +

    ## get banlist

    +

    returns a list of all possibly* banned users
    *(so users for which the osu!web page returned the User not found! ;_; response.)

    +

    >> page size aint configurable, it's 100 atm. maybe a TODO.

    +
      +
    • ENDPOINT: /api/banlist
    • +
    • METHOD: POST
    • +
    • POST FIELDS: page: int (*required, starts at 1)
    • +
    • POST FORMAT: JSON only
    • +
    + Example: +
    + + curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"page": 1}' https://nise.moe/api/banlist + + +

    ## analyze replay file (.osr)

    +

    if you have an .osr file, you can analyze it and get results + an always-online static page for it.

    +
      +
    • ENDPOINT: /api/analyze
    • +
    • METHOD: POST
    • +
    • POST FIELDS: replay: file (*required, an .osr file)
    • +
    + + curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -F "replay=@replay1.osr" https://nise.moe/api/analyze + +

    the response will include an id parameter, which identifies the replay you've uploaded.

    +

    that id will be subsequently available at:

    +
      +
    • WEB INTERFACE: https://nise.moe/c/{id}
    • +
    • API: https://nise.moe/api/user-scores/{id}
    • +
    + +
    + +
    +

    + thanks for using the api. if you have any issues or requests, let me know on discord. +

    +

    plz, try to not abuse it too much.

    +
    diff --git a/nise-frontend/src/app/api/api.component.ts b/nise-frontend/src/app/api/api.component.ts index e467c32..cba9515 100644 --- a/nise-frontend/src/app/api/api.component.ts +++ b/nise-frontend/src/app/api/api.component.ts @@ -2,18 +2,22 @@ import { Component } from '@angular/core'; import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe"; import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component"; import {DatePipe, DecimalPipe, NgForOf, NgIf} from "@angular/common"; +import { + CodeWithCopyButtonComponent +} from "../../corelib/components/code-with-copy-button/code-with-copy-button.component"; @Component({ selector: 'app-api', standalone: true, - imports: [ - CalculatePageRangePipe, - CuteLoadingComponent, - DatePipe, - DecimalPipe, - NgForOf, - NgIf - ], + imports: [ + CalculatePageRangePipe, + CuteLoadingComponent, + DatePipe, + DecimalPipe, + NgForOf, + NgIf, + CodeWithCopyButtonComponent + ], templateUrl: './api.component.html', styleUrl: './api.component.css' }) diff --git a/nise-frontend/src/app/app.component.html b/nise-frontend/src/app/app.component.html index 8b00c5e..6bc3181 100644 --- a/nise-frontend/src/app/app.component.html +++ b/nise-frontend/src/app/app.component.html @@ -35,5 +35,5 @@
    - v20240508 + v20240510
    diff --git a/nise-frontend/src/app/view-user/view-user.component.html b/nise-frontend/src/app/view-user/view-user.component.html index 554105b..35a7fc6 100644 --- a/nise-frontend/src/app/view-user/view-user.component.html +++ b/nise-frontend/src/app/view-user/view-user.component.html @@ -19,6 +19,17 @@ + + +
    +

    + uh oh! this user might be banned. +

    +

    last time ({{ this.userInfo.approximate_ban_date | date: 'medium' }}) we checked their osu!web profile,
    we got the dreaded User not found! ;_; response.

    +
    + +
    +
    diff --git a/nise-frontend/src/app/view-user/view-user.component.ts b/nise-frontend/src/app/view-user/view-user.component.ts index db62113..9e1feed 100644 --- a/nise-frontend/src/app/view-user/view-user.component.ts +++ b/nise-frontend/src/app/view-user/view-user.component.ts @@ -3,7 +3,7 @@ import {SimilarReplay, SuspiciousScore} from "../replays"; import { HttpClient } from "@angular/common/http"; import {catchError, EMPTY, finalize, Observable, Subscription} from "rxjs"; import {environment} from "../../environments/environment"; -import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; +import {DatePipe, DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; import {ActivatedRoute, RouterLink} from "@angular/router"; import {UserDetails, UserQueueDetails} from "../userDetails"; import {calculateTimeAgo, countryCodeToFlag, formatDuration} from "../format"; @@ -21,6 +21,8 @@ interface UserInfo { suspicious_scores: SuspiciousScore[]; similar_replays: SimilarReplay[]; total_scores: number; + is_banned: boolean; + approximate_ban_date: string; } interface UserQueueWebsocketPacket { @@ -42,7 +44,8 @@ interface UserScoresFilter { RouterLink, NgIf, NgOptimizedImage, - CuteLoadingComponent + CuteLoadingComponent, + DatePipe ], templateUrl: './view-user.component.html', styleUrl: './view-user.component.css' diff --git a/nise-frontend/src/assets/style.css b/nise-frontend/src/assets/style.css index a2ffa73..6d19b17 100644 --- a/nise-frontend/src/assets/style.css +++ b/nise-frontend/src/assets/style.css @@ -200,6 +200,11 @@ a.btn-success:hover { border: 1px dotted #b3b8c3; } +.alert-danger { + color: #de7979; + border-color: #de7979; +} + .mb-2 { margin-bottom: 20px; } diff --git a/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.css b/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.css new file mode 100644 index 0000000..e69de29 diff --git a/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.html b/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.html new file mode 100644 index 0000000..f49634c --- /dev/null +++ b/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.html @@ -0,0 +1,7 @@ + + {{ this.textToCopy }} + + + + + diff --git a/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.ts b/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.ts new file mode 100644 index 0000000..b3be3e7 --- /dev/null +++ b/nise-frontend/src/corelib/components/code-with-copy-button/code-with-copy-button.component.ts @@ -0,0 +1,32 @@ +import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input} from '@angular/core'; +import {NgIf} from "@angular/common"; + +@Component({ + selector: 'app-code-with-copy-button', + standalone: true, + imports: [ + NgIf + ], + templateUrl: './code-with-copy-button.component.html', + styleUrl: './code-with-copy-button.component.css' +}) +export class CodeWithCopyButtonComponent implements AfterViewInit { + + @Input() textToCopy!: string; + + constructor(private elRef: ElementRef, private cdr: ChangeDetectorRef) {} + + ngAfterViewInit() { + this.textToCopy = this.elRef.nativeElement.innerText.trim(); + this.cdr.detectChanges(); + } + + copyToClipboard() { + navigator.clipboard.writeText(this.textToCopy).then(() => { + + }).catch(err => { + console.error('could not copy text:', err); + }); + } + +}