Added /api on frontend, identity banned users on profile with alert
This commit is contained in:
parent
f22b139e51
commit
1907c22b16
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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/{replay_id}/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 '{"userId": "degenerate"}' 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 '{"page": 1}' 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=@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/{id}</li>
|
||||
<li><strong>API:</strong> https://nise.moe/api/user-scores/{id}</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>
|
||||
|
||||
@ -2,6 +2,9 @@ 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',
|
||||
@ -12,7 +15,8 @@ import {DatePipe, DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
||||
DatePipe,
|
||||
DecimalPipe,
|
||||
NgForOf,
|
||||
NgIf
|
||||
NgIf,
|
||||
CodeWithCopyButtonComponent
|
||||
],
|
||||
templateUrl: './api.component.html',
|
||||
styleUrl: './api.component.css'
|
||||
|
||||
@ -35,5 +35,5 @@
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<div class="text-center version">
|
||||
v20240508
|
||||
v20240510
|
||||
</div>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -200,6 +200,11 @@ a.btn-success:hover {
|
||||
border: 1px dotted #b3b8c3;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #de7979;
|
||||
border-color: #de7979;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user