mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
feat: Add support for 2018 client API endpoints and data models
- Implemented new search endpoints for rooms to accommodate the 2018 client. - Added support for retrieving room details and featured rooms in a format compatible with the 2018 client. - Introduced new data models for login responses and player presence specific to the 2018 client. - Created mappers to convert between internal entities and the 2018 wire formats for players and rooms. - Updated existing storefront APIs to include versioned endpoints for the 2018 client. - Enhanced the version controller to return dual-format responses for compatibility with both 2018 and 2020 clients. - Refactored the configuration service to ensure trailing slashes in URLs for the 2018 client.
This commit is contained in:
@@ -1,68 +1,104 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DorkNet.Models.Players;
|
||||
using DorkNet.Models.Auth;
|
||||
using DorkNet.Server.Auth;
|
||||
using DorkNet.Server.Compat2018;
|
||||
using DorkNet.Server.Services;
|
||||
|
||||
namespace DorkNet.Server.Controllers.API.Players.V2;
|
||||
|
||||
/// <summary>
|
||||
/// api/players/* for the June-2018 client. This branch is 2018-only, so every
|
||||
/// endpoint returns the 2018 <see cref="Player2018"/> shape (keys Id/Developer/
|
||||
/// JuniorProfile/… — NOT the 2020 PlayerProfile's AccountId/IsDeveloper).
|
||||
///
|
||||
/// The 2018 client mixes path versions: profile fetch + batch are v1, while
|
||||
/// search + displayname are v2. We map both. Param names follow the 2018 client
|
||||
/// (search ?name=, displayname form Name).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]/v2")]
|
||||
[Authorize]
|
||||
public class PlayersController(PlayerService playerService, NotificationService notificationService) : ControllerBase
|
||||
{
|
||||
private long CurrentPlayerId => this.RequireCurrentPlayerId();
|
||||
|
||||
[HttpGet("me")]
|
||||
public async Task<ActionResult<PlayerProfile>> GetSelf()
|
||||
// GET api/players/v1|v2/me
|
||||
[HttpGet("/api/players/v1/me")]
|
||||
[HttpGet("/api/players/v2/me")]
|
||||
public async Task<ActionResult<Player2018>> GetSelf()
|
||||
{
|
||||
var player = await playerService.GetByIdAsync(CurrentPlayerId);
|
||||
if (player is null) return NotFound();
|
||||
return Ok(PlayerService.ToProfile(player));
|
||||
return Ok(Player2018Mapper.From(player));
|
||||
}
|
||||
|
||||
[HttpGet("{id:long}")]
|
||||
public async Task<ActionResult<PlayerProfile>> GetById(long id)
|
||||
// GET api/players/v1|v2/{id} — primary profile fetch (2018 uses v1).
|
||||
[HttpGet("/api/players/v1/{id:long}")]
|
||||
[HttpGet("/api/players/v2/{id:long}")]
|
||||
public async Task<ActionResult<Player2018>> GetById(long id)
|
||||
{
|
||||
var player = await playerService.GetByIdAsync(id);
|
||||
if (player is null) return NotFound();
|
||||
return Ok(PlayerService.ToProfile(player));
|
||||
return Ok(Player2018Mapper.From(player));
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<List<PlayerProfile>>> Search([FromQuery] string query)
|
||||
// POST api/players/v1/list — batch profile resolution (JSON array of ids).
|
||||
[HttpPost("/api/players/v1/list")]
|
||||
[HttpPost("/api/players/v2/list")]
|
||||
public async Task<ActionResult<List<Player2018>>> GetMany([FromBody] long[]? ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return Ok(new List<PlayerProfile>());
|
||||
var results = await playerService.SearchAsync(query);
|
||||
return Ok(results.Select(PlayerService.ToProfile).ToList());
|
||||
if (ids is null || ids.Length == 0) return Ok(new List<Player2018>());
|
||||
var players = await playerService.GetByIdsAsync(ids);
|
||||
return Ok(players.Select(Player2018Mapper.From).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("displayName")]
|
||||
public async Task<ActionResult<PlayerProfile>> UpdateDisplayName([FromForm] string? DisplayName)
|
||||
// GET api/players/v2/search?name= (2018) — also accept ?query= for safety.
|
||||
[HttpGet("/api/players/v1/search")]
|
||||
[HttpGet("/api/players/v2/search")]
|
||||
public async Task<ActionResult<List<Player2018>>> Search(
|
||||
[FromQuery] string? name, [FromQuery] string? query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DisplayName))
|
||||
return BadRequest("DisplayName is required");
|
||||
|
||||
var updated = await playerService.UpdateDisplayNameAsync(CurrentPlayerId, DisplayName);
|
||||
if (!updated) return NotFound();
|
||||
var term = !string.IsNullOrWhiteSpace(name) ? name : query;
|
||||
if (string.IsNullOrWhiteSpace(term)) return Ok(new List<Player2018>());
|
||||
var results = await playerService.SearchAsync(term);
|
||||
return Ok(results.Select(Player2018Mapper.From).ToList());
|
||||
}
|
||||
|
||||
// POST api/players/v2/displayname (2018 form key "Name"). Also accept
|
||||
// "DisplayName" for safety.
|
||||
[HttpPost("/api/players/v1/displayname")]
|
||||
[HttpPost("/api/players/v2/displayname")]
|
||||
public async Task<ActionResult<Player2018>> UpdateDisplayName(
|
||||
[FromForm] string? Name, [FromForm] string? DisplayName)
|
||||
{
|
||||
var newName = !string.IsNullOrWhiteSpace(Name) ? Name : DisplayName;
|
||||
if (string.IsNullOrWhiteSpace(newName)) return BadRequest("Name is required");
|
||||
if (!await playerService.UpdateDisplayNameAsync(CurrentPlayerId, newName)) return NotFound();
|
||||
var player = await playerService.GetByIdAsync(CurrentPlayerId);
|
||||
await notificationService.NotifyAsync(CurrentPlayerId, Models.Notification.PushNotificationId.SubscriptionUpdateProfile, PlayerService.ToProfile(player!));
|
||||
return Ok(PlayerService.ToProfile(player!));
|
||||
await notificationService.NotifyAsync(CurrentPlayerId,
|
||||
Models.Notification.PushNotificationId.SubscriptionUpdateProfile, Player2018Mapper.From(player!));
|
||||
return Ok(Player2018Mapper.From(player!));
|
||||
}
|
||||
|
||||
[HttpPut("{id:long}")]
|
||||
public async Task<ActionResult<PlayerProfile>> UpdateProfile(long id, [FromBody] UpdateProfileRequest req)
|
||||
// POST api/players/v1/bio (2018 form key "Bio").
|
||||
[HttpPost("/api/players/v1/bio")]
|
||||
[HttpPost("/api/players/v2/bio")]
|
||||
public async Task<ActionResult<Player2018>> UpdateBio([FromForm] string? Bio)
|
||||
{
|
||||
if (id != CurrentPlayerId) return Forbid();
|
||||
await playerService.UpdateBioAsync(CurrentPlayerId, Bio ?? string.Empty);
|
||||
var player = await playerService.GetByIdAsync(CurrentPlayerId);
|
||||
return Ok(Player2018Mapper.From(player!));
|
||||
}
|
||||
|
||||
if (req.DisplayName is not null)
|
||||
await playerService.UpdateDisplayNameAsync(id, req.DisplayName);
|
||||
|
||||
if (req.Bio is not null)
|
||||
await playerService.UpdateBioAsync(id, req.Bio);
|
||||
|
||||
var player = await playerService.GetByIdAsync(id);
|
||||
return Ok(PlayerService.ToProfile(player!));
|
||||
// POST api/players/v1/avoidJuniors (form "AvoidJuniors"). Best-effort:
|
||||
// acknowledged so the toggle in the watch settings doesn't 404. (Server
|
||||
// doesn't currently filter matchmaking on this flag.)
|
||||
[HttpPost("/api/players/v1/avoidJuniors")]
|
||||
[HttpPut("/api/players/v1/avoidJuniors")]
|
||||
public async Task<ActionResult<Player2018>> AvoidJuniors()
|
||||
{
|
||||
var player = await playerService.GetByIdAsync(CurrentPlayerId);
|
||||
if (player is null) return NotFound();
|
||||
return Ok(Player2018Mapper.From(player));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user