Add staff playtime feature, including backend services, API endpoint, and frontend integration.
WIP
This commit is contained in:
parent
2be79c180a
commit
8b0d2f9203
|
|
@ -51,6 +51,8 @@ public class SecurityConfig {
|
|||
.requestMatchers("/api/form/**").authenticated()
|
||||
.requestMatchers("/api/login/getUsername").authenticated()
|
||||
.requestMatchers("/api/mail/**").authenticated()
|
||||
.requestMatchers("/api/site/vote").authenticated()
|
||||
.requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@ package com.alttd.altitudeweb.controllers.site;
|
|||
|
||||
import com.alttd.altitudeweb.api.SiteApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||
import com.alttd.altitudeweb.model.StaffPlaytimeListDto;
|
||||
import com.alttd.altitudeweb.model.VoteDataDto;
|
||||
import com.alttd.altitudeweb.model.VoteStatsDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.site.StaffPtService;
|
||||
import com.alttd.altitudeweb.services.site.VoteService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -23,6 +29,18 @@ public class SiteController implements SiteApi {
|
|||
|
||||
private final VoteService voteService;
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
private final StaffPtService staffPtService;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<StaffPlaytimeListDto> getStaffPlaytime(OffsetDateTime from, OffsetDateTime to) {
|
||||
Optional<List<StaffPlaytimeDto>> staffPlaytimeDto = staffPtService.getStaffPlaytime(from.toInstant(), to.toInstant());
|
||||
if (staffPlaytimeDto.isEmpty()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
StaffPlaytimeListDto staffPlaytimeListDto = new StaffPlaytimeListDto();
|
||||
staffPlaytimeListDto.addAll(staffPlaytimeDto.get());
|
||||
return ResponseEntity.ok(staffPlaytimeListDto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<VoteDataDto> getVoteStats() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
||||
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public final class StaffPtToStaffPlaytimeMapper {
|
||||
private record PlaytimeInfo(long totalPlaytime, long lastPlayed) {}
|
||||
|
||||
public List<StaffPlaytimeDto> map(List<StaffPt> sessions, List<Player> staffMembers, long from, long to) {
|
||||
Map<UUID, PlaytimeInfo> playtimeData = getUuidPlaytimeInfoMap(sessions, from, to);
|
||||
|
||||
List<StaffPlaytimeDto> results = new ArrayList<>(playtimeData.size());
|
||||
for (Map.Entry<UUID, PlaytimeInfo> entry : playtimeData.entrySet()) {
|
||||
long lastPlayedMillis = entry.getValue().lastPlayed() == Long.MIN_VALUE ? 0L : entry.getValue().lastPlayed();
|
||||
StaffPlaytimeDto dto = new StaffPlaytimeDto();
|
||||
dto.setStaffMember(staffMembers.stream()
|
||||
.filter(player -> player.uuid().equals(entry.getKey()))
|
||||
.map(Player::username)
|
||||
.findFirst()
|
||||
.orElse(entry.getKey().toString())
|
||||
);
|
||||
dto.setLastPlayed(OffsetDateTime.from(Instant.ofEpochMilli(lastPlayedMillis)));
|
||||
dto.setPlaytime((int) TimeUnit.MILLISECONDS.toMinutes(entry.getValue().totalPlaytime()));
|
||||
results.add(dto);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private Map<UUID, PlaytimeInfo> getUuidPlaytimeInfoMap(List<StaffPt> sessions, long from, long to) {
|
||||
Map<UUID, PlaytimeInfo> playtimeData = new HashMap<>();
|
||||
for (StaffPt session : sessions) {
|
||||
long overlapStart = Math.max(session.sessionStart(), from);
|
||||
long overlapEnd = Math.min(session.sessionEnd(), to);
|
||||
if (overlapEnd <= overlapStart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlaytimeInfo info = playtimeData.getOrDefault(session.uuid(), new PlaytimeInfo(0L, Long.MIN_VALUE));
|
||||
long totalPlaytime = info.totalPlaytime() + (overlapEnd - overlapStart);
|
||||
long lastPlayed = Math.max(info.lastPlayed(), overlapEnd);
|
||||
playtimeData.put(session.uuid(), new PlaytimeInfo(totalPlaytime, lastPlayed));
|
||||
}
|
||||
return playtimeData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.alttd.altitudeweb.services.site;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPlaytimeMapper;
|
||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPt;
|
||||
import com.alttd.altitudeweb.mappers.StaffPtToStaffPlaytimeMapper;
|
||||
import com.alttd.altitudeweb.model.StaffPlaytimeDto;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StaffPtService {
|
||||
private final static String STAFF_GROUPS = "group.admin, group.developer, group.headmod, group.manager, group.moderator, group.owner, group.trainee";
|
||||
private final StaffPtToStaffPlaytimeMapper staffPtToStaffPlaytimeMapper;
|
||||
|
||||
public Optional<List<StaffPlaytimeDto>> getStaffPlaytime(Instant from, Instant to) {
|
||||
CompletableFuture<List<Player>> staffMembersFuture = new CompletableFuture<>();
|
||||
CompletableFuture<List<StaffPt>> staffPlaytimeFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LUCK_PERMS)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading staff members");
|
||||
try {
|
||||
List<Player> staffMemberList = sqlSession.getMapper(TeamMemberMapper.class)
|
||||
.getTeamMembersOfGroupList(STAFF_GROUPS);
|
||||
staffMembersFuture.complete(staffMemberList);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load staff members", e);
|
||||
staffMembersFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
List<Player> staffMembers = staffMembersFuture.join().stream()
|
||||
.collect(Collectors.collectingAndThen(
|
||||
Collectors.toMap(Player::uuid, player -> player, (player1, player2) -> player1),
|
||||
m -> new ArrayList<>(m.values())));
|
||||
Connection.getConnection(Databases.PROXY_PLAYTIME)
|
||||
.runQuery(sqlSession -> {
|
||||
String staffUUIDs = staffMembers.stream()
|
||||
.map(Player::uuid)
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(","));
|
||||
log.debug("Loading staff playtime for group");
|
||||
try {
|
||||
List<StaffPt> sessionsDuring = sqlSession.getMapper(StaffPlaytimeMapper.class)
|
||||
.getSessionsDuring(from.toEpochMilli(), to.toEpochMilli(), staffUUIDs);
|
||||
staffPlaytimeFuture.complete(sessionsDuring);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load staff playtime", e);
|
||||
staffPlaytimeFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
List<StaffPt> join = staffPlaytimeFuture.join();
|
||||
|
||||
return Optional.of(staffPtToStaffPlaytimeMapper.map(join, staffMembers, from.toEpochMilli(), to.toEpochMilli()));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ public enum Databases {
|
|||
LUCK_PERMS("luckperms"),
|
||||
LITE_BANS("litebans"),
|
||||
DISCORD("discordLink"),
|
||||
PROXY_PLAYTIME("proxyplaytime"),
|
||||
VOTING_PLUGIN("votingplugin");
|
||||
|
||||
private final String internalName;
|
||||
|
|
|
|||
|
|
@ -19,4 +19,19 @@ public interface TeamMemberMapper {
|
|||
AND world = 'global'
|
||||
""")
|
||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||
|
||||
@ConstructorArgs({
|
||||
@Arg(column = "username", javaType = String.class),
|
||||
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class)
|
||||
})
|
||||
@Select("""
|
||||
SELECT players.username, players.uuid
|
||||
FROM luckperms_user_permissions AS permissions
|
||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||
WHERE permission IN (${groupPermissions})
|
||||
AND server = 'global'
|
||||
AND world = 'global'
|
||||
""")
|
||||
List<Player> getTeamMembersOfGroupList(@Param("groupPermissions") String groupPermissions);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.alttd.altitudeweb.database.proxyplaytime;
|
||||
|
||||
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||
import org.apache.ibatis.annotations.Arg;
|
||||
import org.apache.ibatis.annotations.ConstructorArgs;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface StaffPlaytimeMapper {
|
||||
@ConstructorArgs({
|
||||
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Arg(column = "serverName", javaType = String.class),
|
||||
@Arg(column = "sessionStart", javaType = long.class),
|
||||
@Arg(column = "sessionEnd", javaType = long.class)
|
||||
})
|
||||
@Select("""
|
||||
SELECT uuid,
|
||||
server_name AS serverName,
|
||||
session_start AS sessionStart,
|
||||
session_end AS sessionEnd
|
||||
FROM sessions
|
||||
WHERE session_end > #{from}
|
||||
AND session_start < #{to}
|
||||
AND uuid IN (${staffUUIDs})
|
||||
ORDER BY uuid, session_start
|
||||
""")
|
||||
List<StaffPt> getSessionsDuring(@Param("from") long from,
|
||||
@Param("to") long to,
|
||||
@Param("staffUUIDs") String staffUUIDs);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.alttd.altitudeweb.database.proxyplaytime;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record StaffPt(UUID uuid, String serverName, long sessionStart, long sessionEnd) {
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ public class Connection {
|
|||
InitializeWebDb.init();
|
||||
InitializeLiteBans.init();
|
||||
InitializeLuckPerms.init();
|
||||
InitializeProxyPlaytime.init();
|
||||
InitializeDiscord.init();
|
||||
InitializeVotingPlugin.init();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.proxyplaytime.StaffPlaytimeMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class InitializeProxyPlaytime {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing ProxyPlaytime");
|
||||
Connection.getConnection(Databases.PROXY_PLAYTIME, (configuration) -> {
|
||||
configuration.addMapper(StaffPlaytimeMapper.class);
|
||||
}).join();
|
||||
log.debug("Initialized ProxyPlaytime");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,6 +14,14 @@ export const routes: Routes = [
|
|||
requiredAuthorizations: ['SCOPE_head_mod']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'staff-pt',
|
||||
loadComponent: () => import('./pages/particles/particles.component').then(m => m.ParticlesComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_head_mod']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<p>staff-pt works!</p>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import {Component, inject, OnInit, signal} from '@angular/core';
|
||||
import {SiteService, StaffPlaytime} from '@api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-staff-pt',
|
||||
imports: [],
|
||||
templateUrl: './staff-pt.component.html',
|
||||
styleUrl: './staff-pt.component.scss'
|
||||
})
|
||||
export class StaffPtComponent implements OnInit {
|
||||
siteService = inject(SiteService);
|
||||
staffPt = signal<StaffPlaytime[]>([])
|
||||
|
||||
ngOnInit(): void {
|
||||
const firstDayOfWeek = new Date();
|
||||
firstDayOfWeek.setDate(firstDayOfWeek.getDate() - firstDayOfWeek.getDay());
|
||||
firstDayOfWeek.setHours(0, 0, 0, 0);
|
||||
const lastDayOfWeek = new Date(firstDayOfWeek);
|
||||
lastDayOfWeek.setDate(firstDayOfWeek.getDate() + 6);
|
||||
lastDayOfWeek.setHours(23, 59, 59, 999);
|
||||
|
||||
this.loadStaffData(firstDayOfWeek, lastDayOfWeek);
|
||||
}
|
||||
|
||||
loadStaffData(from: Date, to: Date) {
|
||||
this.siteService.getStaffPlaytime(from.toISOString(), to.toISOString())
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.staffPt.set(data);
|
||||
},
|
||||
error: err => console.error('Error getting staff playtime:', err)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +144,7 @@
|
|||
<ul class="dropdown">
|
||||
@if (hasAccess([PermissionClaim.HEAD_MOD])) {
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/particles']">Particles</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/staff-pt']">StaffPlaytime</a></li>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ tasks.register< GenerateTask>("generateJavaApi") {
|
|||
typeMappings.put("OffsetDateTime", "Instant")
|
||||
importMappings.put("java.time.OffsetDateTime", "java.time.Instant")
|
||||
modelNameSuffix.set("Dto")
|
||||
|
||||
// Make generator use Java 8 time types and map date-time -> Instant
|
||||
additionalProperties.set(mapOf("dateLibrary" to "java8"))
|
||||
typeMappings.set(mapOf("date-time" to "Instant"))
|
||||
importMappings.set(mapOf("Instant" to "java.time.Instant"))
|
||||
generateModelTests.set(false)
|
||||
generateModelDocumentation.set(false)
|
||||
generateApiTests.set(false)
|
||||
|
|
|
|||
|
|
@ -93,3 +93,5 @@ paths:
|
|||
$ref: './schemas/forms/mail/mail.yml#/GetEmails'
|
||||
/api/site/vote:
|
||||
$ref: './schemas/site/vote.yml#/VoteStats'
|
||||
/api/site/get-staff-playtime/{from}/{to}:
|
||||
$ref: './schemas/site/staff_pt.yml#/GetStaffPlaytime'
|
||||
|
|
|
|||
53
open_api/src/main/resources/schemas/site/staff_pt.yml
Normal file
53
open_api/src/main/resources/schemas/site/staff_pt.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
GetStaffPlaytime:
|
||||
get:
|
||||
tags:
|
||||
- site
|
||||
summary: Get staff playtime for a specified duration
|
||||
description: Get staff playtime for all staff members for a specified duration
|
||||
operationId: getStaffPlaytime
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/From'
|
||||
- $ref: '#/components/parameters/To'
|
||||
responses:
|
||||
'200':
|
||||
description: Staff playtime retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/StaffPlaytimeList'
|
||||
components:
|
||||
parameters:
|
||||
From:
|
||||
name: from
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
example: 2025-01-01T00:00:00.000Z
|
||||
To:
|
||||
name: to
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
example: 2025-01-07T23:59:59.999Z
|
||||
schemas:
|
||||
StaffPlaytimeList:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/StaffPlaytime'
|
||||
StaffPlaytime:
|
||||
type: object
|
||||
properties:
|
||||
staff_member:
|
||||
type: string
|
||||
description: The name of the staff member
|
||||
playtime:
|
||||
type: integer
|
||||
description: Total playtime for the specified duration in minutes
|
||||
last_played:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last played timestamp
|
||||
Loading…
Reference in New Issue
Block a user