Added /api on frontend, identity banned users on profile with alert

This commit is contained in:
nise.moe 2024-06-10 14:28:53 +02:00
parent f22b139e51
commit 1907c22b16
12 changed files with 213 additions and 26 deletions

View File

@ -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(

View File

@ -26,6 +26,8 @@ class UserDetailsController(
val suspicious_scores: List<SuspiciousScoreEntry>,
val similar_replays: List<SimilarReplayEntry>,
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<UserDetailsResponse> {
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)
}

View File

@ -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) {

View File

@ -4,13 +4,19 @@
<p>
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. <u>currently there's no rate limits.</u>
</p>
<div class="alert mb-2 text-center">
<p>
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.
</p>
</div>
<p>as of today, you MUST pass the following parameters:</p>
<ul>
<li>The <code>X-NISE-API</code> header with a value of <code>20240218</code> in every request.</li>
<li>The <code>X-NISE-API</code> header with a value of <code>20240218</code> in every request. This should prevent random shit from breaking in the future (if you keep passing it)</li>
<li>The <code>Accept</code> header with a value of <code>application/json</code> (if you want JSON instead of XML)</li>
</ul>
<h1 class="mt-4">## scores search</h1>
<p>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 <code>AND</code> and <code>OR</code></p>
<p style="font-weight: bold; color: orange">COMING SOON; the route exists but its cancer for api users to form the requests.</p>
<h1 class="mt-4">## get single score</h1>
<p>if you have a <code>replay_id</code>, you can retrieve all the information 'bout that score.</p>
@ -19,7 +25,110 @@
<li><strong>METHOD:</strong> GET</li>
</ul>
Example:
<code style="font-size: 12px">curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439</code>
<br>
<app-code-with-copy-button>
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439
</app-code-with-copy-button>
<h1 class="mt-4">## get single replay (data)</h1>
<p>if you have a <code>replay_id</code>, you can retrieve the data that is used for the replay viewer <i>(replay.nise.moe)</i>
<p>it contains the beatmap data, the full replay data, and all calculated judgements (by circleguard)</p>
<ul>
<li><strong>ENDPOINT:</strong> <code>/api/score/&#123;replay_id&#125;/replay</code></li>
<li><strong>METHOD:</strong> GET</li>
</ul>
Example:
<br>
<app-code-with-copy-button>
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/score/3808640439/replay
</app-code-with-copy-button>
<h1 class="mt-4">## get single replay (.osr)</h1>
<p>if you have a <code>replay_id</code>, you can retrieve the <i>emulated</i> .osr replay file.</p>
<p><i>wtf is an emulated .osr replay file?</i> 🡒 the osu!api does NOT return a "real" replay, so we fill the rest of the details.</p>
<p style="font-weight: bold; color: orange">COMING SOON</p>
<h1 class="mt-4">## get user details</h1>
<p>if you have an <code>user_id</code>, you can retrieve everything we know 'bout that user.
<p style="font-weight: bold; color: #fa5c5c">>> <strong>user_id</strong> is the username. sry.</p>
<ul>
<li><strong>ENDPOINT:</strong> <code>/api/user-details</code></li>
<li><strong>METHOD:</strong> POST</li>
<li><strong>POST FIELDS:</strong> user_id: str (*required)</li>
<li><strong>POST FORMAT:</strong> JSON only</li>
</ul>
Example:
<br>
<app-code-with-copy-button>
curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -H "Content-Type: application/json" -d '&#123;"userId": "degenerate"&#125;' https://nise.moe/api/user-details
</app-code-with-copy-button>
<h1 class="mt-4">## get suspicious scores</h1>
<p>returns a list of suspicious scores (e.g. cvUR <= 25, basically <span style="font-size: 18px" class="board">/sus/</span>)</p>
<p>it is not <i>live</i>, but rather updates every N minutes depending on server load (usually ~10 minutes)</p>
<ul>
<li><strong>ENDPOINT:</strong> <code>/api/suspicious-scores</code></li>
<li><strong>METHOD:</strong> GET</li>
</ul>
Example:
<br>
<app-code-with-copy-button>
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/suspicious-scores
</app-code-with-copy-button>
<h1 class="mt-4">## get stolen replays</h1>
<p>returns a list of all replay pairs with < 10 similarity ratio (basically <span style="font-size: 18px" class="board">/nu/</span>)</p>
<p>it is not <i>live</i>, but rather updates every N minutes depending on server load (usually ~10 minutes)</p>
<ul>
<li><strong>ENDPOINT:</strong> <code>/api/similar-replays</code></li>
<li><strong>METHOD:</strong> GET</li>
</ul>
Example:
<br>
<app-code-with-copy-button>
curl -H "X-NISE-API: 20240218" -H "Accept: application/json" https://nise.moe/api/similar-replays
</app-code-with-copy-button>
<h1 class="mt-4">## get banlist</h1>
<p>returns a list of all <i>possibly<strong>*</strong></i> banned users<br><strong>*</strong>(so users for which the osu!web page returned the <code>User not found! ;_;</code> response.)</p>
<p style="font-weight: bold; color: #ff6dff">>> page size aint configurable, it's 100 atm. maybe a TODO.</p>
<ul>
<li><strong>ENDPOINT:</strong> <code>/api/banlist</code></li>
<li><strong>METHOD:</strong> POST</li>
<li><strong>POST FIELDS:</strong> page: int (*required, starts at 1)</li>
<li><strong>POST FORMAT:</strong> JSON only</li>
</ul>
Example:
<br>
<app-code-with-copy-button>
curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -H "Content-Type: application/json" -d '&#123;"page": 1&#125;' https://nise.moe/api/banlist
</app-code-with-copy-button>
<h1 class="mt-4">## analyze replay file (.osr)</h1>
<p>if you have an .osr file, you can analyze it and get results + an always-online static page for it.</p>
<ul>
<li><strong>ENDPOINT:</strong> <code>/api/analyze</code></li>
<li><strong>METHOD:</strong> POST</li>
<li><strong>POST FIELDS:</strong> replay: file (*required, an .osr file)</li>
</ul>
<app-code-with-copy-button>
curl -X POST -H "X-NISE-API: 20240218" -H "Accept: application/json" -F "replay=&#64;replay1.osr" https://nise.moe/api/analyze
</app-code-with-copy-button>
<p>the response will include an <code>id</code> parameter, which identifies the replay you've uploaded.</p>
<p>that id will be subsequently available at:</p>
<ul>
<li><strong>WEB INTERFACE</strong>: https://nise.moe/c/&#123;id&#125;</li>
<li><strong>API:</strong> https://nise.moe/api/user-scores/&#123;id&#125;</li>
</ul>
<hr>
<div class="mb-2 text-center">
<p>
thanks for using the api. if you have any issues or requests, let me know on discord.
</p>
<p><strong>plz,</strong> try to not abuse it too much.</p>
</div>
</div>
</div>

View File

@ -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'
})

View File

@ -35,5 +35,5 @@
<router-outlet></router-outlet>
<div class="text-center version">
v20240508
v20240510
</div>

View File

@ -19,6 +19,17 @@
</ng-container>
</h1>
<ng-container *ngIf="this.userInfo.is_banned && this.userInfo.approximate_ban_date">
<div class="alert alert-danger mb-2 text-center">
<p>
uh oh! this user <i>might</i> be banned.
</p>
<p>last time ({{ this.userInfo.approximate_ban_date | date: 'medium' }}) we checked their osu!web profile,<br> we got the dreaded <code>User not found! ;_;</code> response.</p>
</div>
</ng-container>
<div class="mb-2 mt-2 btn-group">
<a [href]="'https://osu.ppy.sh/users/' + this.userInfo.user_details.user_id + '/osu'"
class="btn btn-outline-secondary btn-sm" target="_blank">

View File

@ -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'

View File

@ -200,6 +200,11 @@ a.btn-success:hover {
border: 1px dotted #b3b8c3;
}
.alert-danger {
color: #de7979;
border-color: #de7979;
}
.mb-2 {
margin-bottom: 20px;
}

View File

@ -0,0 +1,7 @@
<ng-container *ngIf="this.textToCopy; else ngContent">
<code style="font-size: 12px; margin-right: 10px">{{ this.textToCopy }}</code> <button (click)="copyToClipboard()">copy</button>
</ng-container>
<ng-template #ngContent>
<ng-content ></ng-content>
</ng-template>

View File

@ -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);
});
}
}