mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using RadicadeServer.RecNet.Enums.Moderation;
|
||||
|
||||
namespace RadicadeServer.RecNet.Moderation
|
||||
{
|
||||
public class Reputation
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public const int CheerBadgeThreshold = 5;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public const double DefaultReputation = 10.0;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public const double NoterietyPerCheer = 0.5;
|
||||
public const double NoterietyPerSubscriber = 0.5;
|
||||
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
||||
public long AccountId { get; set; }
|
||||
public double Noteriety { get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public double ReputationScore { get; set; } = DefaultReputation;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public int ReportCount { 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; }
|
||||
public int CheerCredit { get; set; }
|
||||
public int SelectedCheer { get; set; }
|
||||
public bool IsCheerful { get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[NotMapped]
|
||||
public int TotalCheers =>
|
||||
CheerGeneral + CheerHelpful + CheerCreative + CheerGreatHost + CheerSportsman + CheerCredit;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[NotMapped]
|
||||
public string Trust => ReputationScore switch
|
||||
{
|
||||
>= 8.0 => "Trusted",
|
||||
>= 6.0 => "Neutral",
|
||||
_ => "Not Trusted"
|
||||
};
|
||||
|
||||
public int GetCheerCount(CheerCategory category) => category switch
|
||||
{
|
||||
CheerCategory.General => CheerGeneral,
|
||||
CheerCategory.Helpful => CheerHelpful,
|
||||
CheerCategory.Creative => CheerCreative,
|
||||
CheerCategory.GreatHost => CheerGreatHost,
|
||||
CheerCategory.Sportsman => CheerSportsman,
|
||||
CheerCategory.Credit => CheerCredit,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void AddCheer(CheerCategory category)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case CheerCategory.General: CheerGeneral++; break;
|
||||
case CheerCategory.Helpful: CheerHelpful++; break;
|
||||
case CheerCategory.Creative: CheerCreative++; break;
|
||||
case CheerCategory.GreatHost: CheerGreatHost++; break;
|
||||
case CheerCategory.Sportsman: CheerSportsman++; break;
|
||||
case CheerCategory.Credit: CheerCredit++; break;
|
||||
}
|
||||
|
||||
Noteriety += NoterietyPerCheer;
|
||||
IsCheerful = CheerBadgeEarned;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void AddSubscriber()
|
||||
{
|
||||
Noteriety += NoterietyPerSubscriber;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void ApplyBanPenalty(bool permanent, bool cheating)
|
||||
{
|
||||
double penalty = permanent ? 4.0 : (cheating ? 1.0 : 0.5);
|
||||
ReputationScore = Math.Max(0.0, ReputationScore - penalty);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public Reputation ForClient()
|
||||
{
|
||||
return new Reputation
|
||||
{
|
||||
AccountId = AccountId,
|
||||
Noteriety = Math.Min(0.05, Noteriety / 100.0),
|
||||
ReputationScore = ReputationScore,
|
||||
|
||||
|
||||
|
||||
ReportCount = ReportCount,
|
||||
CheerGeneral = CheerGeneral,
|
||||
CheerHelpful = CheerHelpful,
|
||||
CheerCreative = CheerCreative,
|
||||
CheerGreatHost = CheerGreatHost,
|
||||
CheerSportsman = CheerSportsman,
|
||||
CheerCredit = CheerCredit,
|
||||
SelectedCheer = SelectedCheer,
|
||||
|
||||
|
||||
|
||||
IsCheerful = CheerBadgeEarned
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[NotMapped]
|
||||
public bool CheerBadgeEarned => TotalCheers >= CheerBadgeThreshold;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
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())
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user