Files
Radicade/Server/PlayerReputation/ReputationAPI.cs
T
2026-10-16 23:33:13 -07:00

289 lines
8.2 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.API.Notifications;
using RadicadeServer.RecNet.Enums.Messages;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Messages;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.RecNet.Notifications;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using RadicadeServer.RecNet.Responses;
namespace RadicadeServer.API.Moderation
{
[Route("api/playerReputation")]
public class ReputationAPI : Controller
{
RecDBContext db;
public ReputationAPI(RecDBContext context) {
this.db = context;
}
[HttpGet("v1/{id}")]
public IActionResult GetReputation(long id)
{
var reputation = db.Reputations.FirstOrDefault((x) => x.AccountId == id);
reputation ??= new Reputation { AccountId = id };
return Ok(CreateClientReputation(reputation.ForClient()));
}
[HttpPost("v1/bulk")]
public IActionResult GetBulkReputation([FromForm] long[] ids)
{
var reputations = new List<ClientReputation>();
foreach (var id in ids)
{
var reputation = db.Reputations.FirstOrDefault((x) => x.AccountId == id);
reputation ??= new Reputation { AccountId = id };
reputations.Add(CreateClientReputation(reputation.ForClient()));
}
return Ok(reputations);
}
[HttpGet("v2/bulk")]
public IActionResult GetBulkReputationV2([FromQuery] long[] id)
{
var reputations = new List<ClientReputation>();
foreach (var i in id)
{
var reputation = db.Reputations.FirstOrDefault((x) => x.AccountId == i);
reputation ??= new Reputation { AccountId = i };
reputations.Add(CreateClientReputation(reputation.ForClient()));
}
return Ok(reputations);
}
private static ClientReputation CreateClientReputation(Reputation reputation) => new()
{
Id = reputation.AccountId,
AccountId = reputation.AccountId,
IsCheerful = true,
Noteriety = (float)reputation.Noteriety,
SelectedCheer = reputation.SelectedCheer,
CheerCredit = reputation.CheerCredit > 0 ? reputation.CheerCredit : 20,
CheerGeneral = reputation.CheerGeneral,
CheerHelpful = reputation.CheerHelpful,
CheerCreative = reputation.CheerCreative,
CheerGreatHost = reputation.CheerGreatHost,
CheerSportsman = reputation.CheerSportsman
};
private sealed class ClientReputation
{
public long Id { get; set; }
[JsonPropertyName("accountId")]
public long AccountId { get; set; }
public bool IsCheerful { get; set; }
public float Noteriety { get; set; }
public int SelectedCheer { get; set; }
public int CheerCredit { get; set; }
public int CheerGeneral { get; set; }
public int CheerHelpful { get; set; }
public int CheerCreative { get; set; }
public int CheerGreatHost { get; set; }
public int CheerSportsman { get; set; }
}
private static readonly CheerCategory[] AwardableCheers =
{
CheerCategory.General, CheerCategory.Helpful, CheerCategory.Sportsman,
CheerCategory.GreatHost, CheerCategory.Creative
};
[HttpPost("~/api/PlayerCheer/v1/create")]
[HttpPost("v1/cheer"), Authorize]
public async Task<ActionResult<Success>> CheerPlayer()
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var request = await ReadRequestValuesAsync();
var values = request.Values;
var playerId = ReadLong(values, "playeridto", "playerid", "player", "targetplayerid", "accountid", "id");
var category = ReadInt(values, "category", "cheercategory", "cheer", "value");
if (playerId == null || category == null)
{
return BadRequest(Success.Error("No player or cheer category was given."));
}
if (playerId == userId) return BadRequest(Success.Error("You can't cheer yourself."));
var cheer = (CheerCategory)category.Value;
if (!AwardableCheers.Contains(cheer))
return BadRequest(Success.Error($"Unknown cheer category {category.Value}."));
var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == playerId);
if (reputation == null)
{
reputation = new Reputation { AccountId = playerId.Value };
await db.Reputations.AddAsync(reputation);
}
reputation.AddCheer(cheer);
await db.SaveChangesAsync();
await Notify.SendMessage(playerId.Value, new Message
{
FromPlayerId = userId.Value,
Type = MessageType.PlayerCheer,
Data = category.Value.ToString()
});
await Notify.SendAccountUpdate(playerId.Value);
await SendReputationUpdate(playerId.Value, reputation);
return Ok(Success.Ok);
}
private async Task<(Dictionary<string, string> Values, string Raw)> ReadRequestValuesAsync()
{
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var (key, value) in Request.RouteValues)
if (value != null) values[key] = value.ToString() ?? string.Empty;
foreach (var (key, value) in Request.Query)
values[key] = value.ToString();
Request.EnableBuffering();
string raw;
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true))
raw = (await reader.ReadToEndAsync()).Trim();
Request.Body.Position = 0;
if (raw.Length == 0) return (values, raw);
if (raw.StartsWith('{'))
{
try
{
if (JsonNode.Parse(raw) is JsonObject json)
{
foreach (var (key, value) in json)
values[key] = value?.ToString() ?? string.Empty;
return (values, raw);
}
}
catch (JsonException) { }
}
if (raw.Contains('='))
{
foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var parts = pair.Split('=', 2);
if (parts.Length == 2)
values[parts[0]] = Uri.UnescapeDataString(parts[1]);
}
return (values, raw);
}
values["category"] = raw;
return (values, raw);
}
private static int? ReadInt(Dictionary<string, string> values, params string[] keys)
{
foreach (var key in keys)
if (values.TryGetValue(key, out var raw) && int.TryParse(raw, out var parsed))
return parsed;
return null;
}
private static long? ReadLong(Dictionary<string, string> values, params string[] keys)
{
foreach (var key in keys)
if (values.TryGetValue(key, out var raw) && long.TryParse(raw, out var parsed))
return parsed;
return null;
}
[HttpPost("~/api/PlayerCheer/v1/SetSelectedCheer")]
[HttpPost("v1/selectedCheer"), HttpPost("v1/selectCheer"),
HttpPost("v1/selectedCheer/{category:int}"), HttpPut("v1/selectedCheer"), Authorize]
public async Task<ActionResult<Success>> SetSelectedCheer()
{
var userId = User.GetUserId();
if (userId == null) return Unauthorized();
var request = await ReadRequestValuesAsync();
var category = ReadInt(request.Values, "category", "cheercategory", "cheer", "value", "selectedcheer");
if (category == null)
{
return BadRequest(Success.Error("No cheer category was given."));
}
var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == userId);
if (reputation == null)
{
reputation = new Reputation
{
AccountId = userId.Value,
IsCheerful = true,
CheerCredit = 20
};
await db.Reputations.AddAsync(reputation);
}
reputation.SelectedCheer = category.Value;
await db.SaveChangesAsync();
await Notify.SendAccountUpdate(userId.Value);
await SendReputationUpdate(userId.Value, reputation);
return Ok(Success.Ok);
}
/// <summary>
/// The frame the cheer counters and the cheer badge are built from, pushed under
/// "ReputationUpdate" - the key the 2023 client registers a handler for. The account update
/// only carries the profile, so without this the cheer a player was just handed, and the
/// badge it unlocked, stayed invisible on the receiving client until their next login
/// refetched the reputation, which is what "not updated instantly" looked like.
/// </summary>
private static Task SendReputationUpdate(long accountId, Reputation reputation) =>
Notify.SendNotify(accountId, new Notification
{
Id = "ReputationUpdate",
Msg = CreateClientReputation(reputation.ForClient())
});
}
}