banlist/follow page stuff

This commit is contained in:
nise.moe 2024-03-08 09:18:10 +01:00
parent d291b19a6a
commit 889d2c40e4
13 changed files with 266 additions and 67 deletions

View File

@ -73,8 +73,14 @@ class SecurityConfig {
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/user-queue").authenticated()
.requestMatchers( "/follows", "/follows/**").authenticated()
.anyRequest().permitAll()
}
.exceptionHandling {
it.authenticationEntryPoint { _, response, _ ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
}
}
.oauth2Login { oauthLogin ->
oauthLogin.successHandler(CustomAuthenticationSuccessHandler())
}

View File

@ -1,16 +1,11 @@
package com.nisemoe.nise.controller
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.generated.tables.references.USER_FOLLOWS
import com.nisemoe.nise.service.AuthService
import jakarta.validation.Valid
import jakarta.validation.constraints.Size
import jakarta.validation.constraints.Min
import org.jooq.DSLContext
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import java.time.OffsetDateTime
@ -20,15 +15,84 @@ class BanlistController(
private val dslContext: DSLContext
) {
data class BanStatisticsResponse(
val totalUsersBanned: Int
companion object {
const val MAX_BANLIST_ENTRIES_PER_PAGE = 100
}
data class BanlistRequest(
@field:Min(1)
val page: Int,
)
@GetMapping("banlist/statistics")
fun getBanStatistics(): BanStatisticsResponse {
val totalUsersBanned = dslContext.fetchCount(USERS, USERS.IS_BANNED.eq(true))
data class BanlistResponse(
val pagination: BanlistPagination,
val users: List<BanlistEntry>
)
return BanStatisticsResponse(totalUsersBanned)
data class BanlistPagination(
val currentPage: Int,
val pageSize: Int,
val totalResults: Int
) {
val totalPages: Int
get() = if (totalResults % pageSize == 0) totalResults / pageSize else totalResults / pageSize + 1
}
data class BanlistEntry(
val userId: Long,
val username: String,
val secondsPlayed: Long?,
val pp: Double?,
val rank: Long?,
val isBanned: Boolean?,
val approximateBanTime: OffsetDateTime?,
val lastUpdate: OffsetDateTime?
)
@PostMapping("banlist")
fun banUser(@RequestBody @Valid request: BanlistRequest): ResponseEntity<BanlistResponse> {
//
val pagination = BanlistPagination(
request.page,
MAX_BANLIST_ENTRIES_PER_PAGE,
dslContext.fetchCount(USERS, USERS.IS_BANNED.eq(true))
)
dslContext.select(
USERS.USER_ID,
USERS.USERNAME,
USERS.SECONDS_PLAYED,
USERS.IS_BANNED,
USERS.PP_RAW,
USERS.RANK,
USERS.SYS_LAST_UPDATE,
USERS.APPROX_BAN_DATE
)
.from(USERS)
.where(USERS.IS_BANNED.eq(true))
.orderBy(USERS.APPROX_BAN_DATE.desc())
.limit(MAX_BANLIST_ENTRIES_PER_PAGE)
.offset((request.page - 1) * 10)
.fetch()
.map {
BanlistEntry(
it[USERS.USER_ID]!!,
it[USERS.USERNAME]!!,
it[USERS.SECONDS_PLAYED],
it[USERS.PP_RAW],
it[USERS.RANK],
it[USERS.IS_BANNED],
it[USERS.APPROX_BAN_DATE],
it[USERS.SYS_LAST_UPDATE]
)
}
.let {
return ResponseEntity.ok(BanlistResponse(pagination, it))
}
}
}

View File

@ -40,10 +40,6 @@ class FollowsController(
@GetMapping("follows")
fun getFollowsBanStatus(): ResponseEntity<FollowsBanStatusResponse> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
val follows = dslContext.select(
USERS.USER_ID,
USERS.USERNAME,
@ -70,10 +66,6 @@ class FollowsController(
@GetMapping("follows/{followsUserId}")
fun getFollowsBanStatusByUserId(@PathVariable followsUserId: Long): ResponseEntity<FollowsBanStatusEntry> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
val userId = authService.getCurrentUser().userId
val follows = dslContext.select(
@ -109,10 +101,6 @@ class FollowsController(
@PutMapping("follows")
fun updateFollowsBanStatus(@RequestBody @Valid request: UpdateFollowsBanStatusRequest): ResponseEntity<Void> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
// Check if the user already has MAX_FOLLOWS_PER_USER or more
if(dslContext.fetchCount(USER_FOLLOWS, USER_FOLLOWS.USER_ID.eq(authService.getCurrentUser().userId)) >= MAX_FOLLOWS_PER_USER) {
return ResponseEntity.status(400).build()
@ -138,10 +126,6 @@ class FollowsController(
@DeleteMapping("follows")
fun deleteFollowsBanStatus(@RequestBody @Valid request: DeleteFollowsBanStatusRequest): ResponseEntity<Void> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
val userId = authService.getCurrentUser().userId
for(userIdToUnban in request.userIds) {

View File

@ -344,6 +344,7 @@ class ImportScores(
.execute()
if(isBanned == true) {
this.messagingTemplate.convertAndSend("/topic/banlist", userId)
dslContext.update(SCORES)
.set(SCORES.IS_BANNED, true)
.where(SCORES.USER_ID.eq(userId))

View File

@ -10,6 +10,7 @@ import org.jooq.DSLContext
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Profile
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.time.OffsetDateTime
@ -21,6 +22,7 @@ class ImportUsers(
private val userService: UserService,
private val discordService: DiscordService,
private val osuApi: OsuApi,
private val messagingTemplate: SimpMessagingTemplate
) {
private val logger = LoggerFactory.getLogger(javaClass)
@ -103,6 +105,7 @@ class ImportUsers(
Thread.sleep(SLEEP_AFTER_API_CALL)
if (isUserBanned == true) {
this.logger.warn("User $missingId is banned")
this.messagingTemplate.convertAndSend("/topic/banlist", missingId)
dslContext.update(SCORES)
.set(SCORES.IS_BANNED, true)
.where(SCORES.USER_ID.eq(missingId))

View File

@ -9,6 +9,7 @@ import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.compo
import {SearchComponent} from "./search/search.component";
import {ContributeComponent} from "./contribute/contribute.component";
import {BanlistComponent} from "./banlist/banlist.component";
import {FollowsComponent} from "./follows/follows.component";
const routes: Routes = [
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
@ -24,8 +25,10 @@ const routes: Routes = [
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
{path: 'follows', component: FollowsComponent, title: '/follows/'},
{path: 'banlist', component: BanlistComponent, title: '/ban/'},
{path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'},
{path: '**', component: HomeComponent, title: '/nise.moe/'},
];

View File

@ -1,27 +1,71 @@
<div class="main term mb-2">
<div class="fade-stuff">
<h1 class="mb-4"># follow-list</h1>
<table *ngIf="this.follows">
<h1 class="mb-4"># recent bans</h1>
<div class="alert mb-2 text-center">
<p>
just because an user appears on this list, <u>it doesn't mean they were banned for cheating.</u>
</p>
<p>there are more a multitude of reasons osu!support might close an account.</p>
<p>
all we do is check if the user exists (with their unique id) and if we get a "hey, it's missing" response (aka the <code>User not found! ;_;</code> message) we mark it as possibly banned.
</p>
</div>
<ng-container *ngIf="this.isLoading">
<div class="text-center">
<p>Loading <app-cute-loading></app-cute-loading></p>
<p>please be patient - the database is working hard!</p>
</div>
</ng-container>
<ng-template #nullTemplate>
<code>null</code>
</ng-template>
<table *ngIf="this.banlist">
<thead>
<tr>
<th colspan="2">Username</th>
<th>Is banned?</th>
<th>Time played</th>
<th>Total PP</th>
<th>Rank</th>
<th>Last check</th>
<th>Approximate ban date</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of this.follows.follows">
<tr *ngFor="let user of this.banlist.users">
<td>
<img [src]="'https://a.ppy.sh/' + user.userId" class="avatar" style="width: 16px; min-height: 16px; height: 16px;">
<img [src]="'https://a.ppy.sh/' + user.userId" class="avatar" style="width: 16px; min-height: 16px; height: 16px;" loading="lazy">
</td>
<td>
<a [routerLink]="['/u', user.username]">
{{ user.username }}
</a>
</td>
<td>{{ user.isBanned }}</td>
<td>{{ calculateTimeAgo(user.lastUpdate) }}</td>
<td>
<ng-container *ngIf="user.secondsPlayed else nullTemplate">
{{ formatDuration(user.secondsPlayed) }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.pp; else nullTemplate">
{{ user.pp | number: '1.0-0' }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.rank; else nullTemplate">
#{{ user.rank | number }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.lastUpdate; else nullTemplate">
{{ calculateTimeAgo(user.lastUpdate) }}
</ng-container>
</td>
<td>
{{ user.approximateBanTime | date: 'medium' }}
</td>
<td>
</td>
</tbody>

View File

@ -1,26 +1,35 @@
import {Component, OnInit} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {JsonPipe, NgForOf, NgIf} from "@angular/common";
import {calculateTimeAgo} from "../format";
import {DatePipe, DecimalPipe, JsonPipe, NgForOf, NgIf} from "@angular/common";
import {calculateTimeAgo, formatDuration} from "../format";
import {RouterLink} from "@angular/router";
import {CuteProgressbarComponent} from "../../corelib/components/cute-progressbar/cute-progressbar.component";
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
interface BanStatisticsResponse {
totalUsersBanned: number;
interface BanlistResponse {
pagination: BanlistPagination;
users: BanlistEntry[];
}
interface FollowsBanStatusResponse {
follows: FollowsBanStatusEntry[];
interface BanlistPagination {
currentPage: number;
pageSize: number;
totalResults: number;
totalPages: number;
}
interface FollowsBanStatusEntry {
interface BanlistEntry {
userId: number;
username: string;
isBanned: boolean;
lastUpdate: string;
secondsPlayed?: number;
pp?: number;
rank?: number;
isBanned?: boolean;
approximateBanTime?: string;
lastUpdate?: string;
}
@Component({
selector: 'app-banlist',
standalone: true,
@ -28,34 +37,36 @@ interface FollowsBanStatusEntry {
JsonPipe,
NgForOf,
NgIf,
RouterLink
RouterLink,
DatePipe,
DecimalPipe,
CuteProgressbarComponent,
CuteLoadingComponent
],
templateUrl: './banlist.component.html',
styleUrl: './banlist.component.css'
})
export class BanlistComponent implements OnInit {
banStatistics: BanStatisticsResponse | null = null
follows: FollowsBanStatusResponse | null = null;
isLoading = true;
banlist: BanlistResponse | null = null;
constructor(private httpClient: HttpClient) { }
ngOnInit(): void {
this.getBanStatistics();
this.getFollows();
this.getBanlist();
}
getBanStatistics() {
this.httpClient.get<BanStatisticsResponse>(`${environment.apiUrl}/banlist/statistics`).subscribe(response => {
this.banStatistics = response;
});
getBanlist(): void {
const body = {
page: 1
}
getFollows(): void {
this.httpClient.get<FollowsBanStatusResponse>(`${environment.apiUrl}/follows`).subscribe(response => {
this.follows = response;
this.httpClient.post<BanlistResponse>(`${environment.apiUrl}/banlist`, body).subscribe(response => {
this.banlist = response;
this.isLoading = false;
});
}
protected readonly calculateTimeAgo = calculateTimeAgo;
protected readonly formatDuration = formatDuration;
}

View File

@ -0,0 +1,3 @@
table td {
text-align: center;
}

View File

@ -0,0 +1,31 @@
<div class="main term mb-2">
<div class="fade-stuff">
<h1 class="mb-4"># follow-list</h1>
<table *ngIf="this.follows">
<thead>
<tr>
<th colspan="2">Username</th>
<th>Is banned?</th>
<th>Last check</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of this.follows.follows">
<td>
<img [src]="'https://a.ppy.sh/' + user.userId" class="avatar" style="width: 16px; min-height: 16px; height: 16px;">
</td>
<td>
<a [routerLink]="['/u', user.username]">
{{ user.username }}
</a>
</td>
<td>{{ user.isBanned }}</td>
<td>{{ calculateTimeAgo(user.lastUpdate) }}</td>
<td>
</td>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,49 @@
import {Component, OnInit} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {JsonPipe, NgForOf, NgIf} from "@angular/common";
import {calculateTimeAgo} from "../format";
import {RouterLink} from "@angular/router";
interface FollowsBanStatusResponse {
follows: FollowsBanStatusEntry[];
}
interface FollowsBanStatusEntry {
userId: number;
username: string;
isBanned: boolean;
lastUpdate: string;
}
@Component({
selector: 'app-follows',
standalone: true,
imports: [
JsonPipe,
NgForOf,
NgIf,
RouterLink
],
templateUrl: './follows.component.html',
styleUrl: './follows.component.css'
})
export class FollowsComponent implements OnInit {
follows: FollowsBanStatusResponse | null = null;
constructor(private httpClient: HttpClient) { }
ngOnInit(): void {
this.getFollows();
}
getFollows(): void {
this.httpClient.get<FollowsBanStatusResponse>(`${environment.apiUrl}/follows`).subscribe(response => {
this.follows = response;
});
}
protected readonly calculateTimeAgo = calculateTimeAgo;
}

View File

@ -19,9 +19,3 @@
.score-entry:hover {
background-color: rgba(179, 184, 195, 0.15);
}
code {
background-color: rgba(227, 232, 255, 0.1);
padding: 2px;
border-radius: 3px;
}

View File

@ -14,6 +14,12 @@
src: url(ia-quattro-700-normal.woff2) format('woff2');
}
code {
background-color: rgba(227, 232, 255, 0.1);
padding: 2px;
border-radius: 3px;
}
html {
background-color: #151515;
@ -190,7 +196,7 @@ a.btn-success:hover {
}
.alert {
padding: 5px;
padding: 8px;
border: 1px dotted #b3b8c3;
}