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(); 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(); 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> 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 Values, string Raw)> ReadRequestValuesAsync() { var values = new Dictionary(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 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 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> 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); } /// /// 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. /// private static Task SendReputationUpdate(long accountId, Reputation reputation) => Notify.SendNotify(accountId, new Notification { Id = "ReputationUpdate", Msg = CreateClientReputation(reputation.ForClient()) }); } }