using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DorkNet.Models.Notification; using DorkNet.Server.Auth; using DorkNet.Server.Data; using DorkNet.Server.Data.Entities; using DorkNet.Server.Services; namespace DorkNet.Server.Controllers.API.PlayerCheer; /// /// api.{rec.net,localhost}/api/PlayerCheer/v1/* — peer cheer flow. /// Cheers between players are stored as /// rows keyed by TargetPlayerId. The "selected cheer" badge /// each player can pin to their profile is stored as a /// with key SelectedCheer. /// [ApiController] [Authorize] public class PlayerCheerController( DorkNetDbContext db, NotificationService notifications, LevelService level) : ControllerBase { private long Me => this.RequireCurrentPlayerId(); /// POST api/PlayerCheer/v1/create — caller cheers /// the target player. Type matches the same enum as /// CheerEntity.Type. Idempotent on (caller, target, type) /// to match PlayerStateController.CheerPlayer, so the same /// category click doesn't inflate the count. [HttpPost("api/PlayerCheer/v1/create")] [Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")] public async Task Create( [FromForm(Name = "TargetAccountId")] long? targetAccountId, [FromForm(Name = "PlayerIdTo")] long? playerIdTo, [FromForm(Name = "Type")] int type = 0, [FromForm(Name = "CheerCategory")] int? cheerCategory = null) { // 2018 client posts PlayerIdTo + CheerCategory (RecNet.cs); 2020 posts // TargetAccountId + Type. Accept both. type = cheerCategory ?? type; if ((targetAccountId ?? playerIdTo) is not long target || target <= 0) return Ok(new { success = false, error = "missing_target" }); if (Me == target) return Ok(new { success = false, error = "cannot_cheer_self" }); var existing = await db.Cheers.FirstOrDefaultAsync(c => c.FromPlayerId == Me && c.TargetPlayerId == target && c.TargetRoomId == 0 && c.Type == type); if (existing is not null) return Ok(new { success = true, already_cheered = true }); db.Cheers.Add(new CheerEntity { FromPlayerId = Me, TargetPlayerId = target, Type = type, }); await db.SaveChangesAsync(); await level.AwardXpAsync(target, LevelService.CheerReceivedXp, $"cheer_from:{Me}"); await notifications.NotifyAsync(target, PushNotificationId.SubscriptionUpdateProfile, new { Reason = "CheerReceived", From = Me, Type = type }); return Ok(new { success = true, error = "" }); } /// POST api/PlayerCheer/v1/SetSelectedCheer — pin /// a cheer category as the caller's selected badge. Stored on the /// PlayerSetting key SelectedCheer (string-form int). [HttpPost("api/PlayerCheer/v1/SetSelectedCheer")] [Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")] public async Task SetSelectedCheer( [FromForm(Name = "Cheer")] int cheer = 0, [FromForm(Name = "CheerCategory")] int? cheerCategory = null) { cheer = cheerCategory ?? cheer; // 2018 sends CheerCategory var existing = await db.PlayerSettings .FirstOrDefaultAsync(s => s.PlayerId == Me && s.Key == "SelectedCheer"); if (existing is null) db.PlayerSettings.Add(new PlayerSettingEntity { PlayerId = Me, Key = "SelectedCheer", Value = cheer.ToString() }); else existing.Value = cheer.ToString(); await db.SaveChangesAsync(); return Ok(new { success = true, error = "" }); } /// GET api/PlayerCheer/v1/all — every cheer the /// caller has received. Drives the profile's "X cheers" badge. [HttpGet("api/PlayerCheer/v1/all")] public async Task All() { var rows = await db.Cheers .Where(c => c.TargetPlayerId == Me) .OrderByDescending(c => c.CheeredAt) .Take(200) .Select(c => new { Id = c.Id, FromPlayerId = c.FromPlayerId, Type = c.Type, CheeredAt = c.CheeredAt, }) .ToListAsync(); return Ok(rows); } }