feat: Remove unnecessary push notifications for avatar updates in AdminController; update comments for clarity

This commit is contained in:
Alexa
2026-07-12 13:18:32 +03:00
parent 28b5d5525b
commit 5fcef3ffe7
2 changed files with 41 additions and 63 deletions
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using DorkNet.Models.Auth;
using DorkNet.Server.Auth; using DorkNet.Server.Auth;
using DorkNet.Server.Data; using DorkNet.Server.Data;
using DorkNet.Server.Data.Entities; using DorkNet.Server.Data.Entities;
@@ -11,6 +10,14 @@ namespace DorkNet.Server.Controllers.API.InfluencerPartnerProgram;
[ApiController] [ApiController]
public class InfluencerPartnerProgramController(DorkNetDbContext db) : ControllerBase public class InfluencerPartnerProgramController(DorkNetDbContext db) : ControllerBase
{ {
// The 2023 client's influencer API (RecNet.Runtime IBEEFFGHGND) reads
// BARE scalars, NOT wrapped account objects:
// influencers → List<Int32> (account ids)
// influencer → Nullable<Int32> (the account id, or null)
// myinfluencer → Nullable<Int32> (supported influencer's id, or null)
// Returning an object here crashes the strict reader
// ("expected:'Number Token', actual:'{'"), which is what broke the
// profile view (and the cheer flow that opens it).
[HttpGet("api/influencerpartnerprogram/influencers")] [HttpGet("api/influencerpartnerprogram/influencers")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Influencers([FromQuery] int take = 50) public async Task<IActionResult> Influencers([FromQuery] int take = 50)
@@ -22,29 +29,17 @@ public class InfluencerPartnerProgramController(DorkNetDbContext db) : Controlle
.Distinct() .Distinct()
.Take(take) .Take(take)
.ToListAsync(); .ToListAsync();
var players = await db.Players return Ok(creatorIds.Select(id => (int)id).ToArray());
.Where(p => creatorIds.Contains(p.Id))
.OrderBy(p => p.Username)
.ToListAsync();
var influencers = players.Select(ToInfluencerWire).ToList();
return Ok(new Dictionary<string, object>
{
["Influencers"] = influencers,
["influencers"] = influencers,
["Accounts"] = influencers,
["accounts"] = influencers,
});
} }
[HttpGet("api/influencerpartnerprogram/influencer")] [HttpGet("api/influencerpartnerprogram/influencer")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Influencer([FromQuery] long? accountId = null, [FromQuery] string? username = null) public async Task<IActionResult> Influencer([FromQuery] long? accountId = null, [FromQuery] string? username = null)
{ {
var player = accountId is long id var playerId = accountId is long id
? await db.Players.FirstOrDefaultAsync(p => p.Id == id) ? await db.Players.Where(p => p.Id == id).Select(p => (long?)p.Id).FirstOrDefaultAsync()
: await db.Players.FirstOrDefaultAsync(p => p.Username == username); : await db.Players.Where(p => p.Username == username).Select(p => (long?)p.Id).FirstOrDefaultAsync();
if (player is null) return NotFound(); return Content(playerId is long pid ? ((int)pid).ToString() : "null", "application/json");
return Ok(ToInfluencerWire(player));
} }
[HttpGet("api/influencerpartnerprogram/myinfluencer")] [HttpGet("api/influencerpartnerprogram/myinfluencer")]
@@ -54,10 +49,11 @@ public class InfluencerPartnerProgramController(DorkNetDbContext db) : Controlle
var me = this.RequireCurrentPlayerId(); var me = this.RequireCurrentPlayerId();
var setting = await db.PlayerSettings var setting = await db.PlayerSettings
.FirstOrDefaultAsync(s => s.PlayerId == me && s.Key == "influencer:supported"); .FirstOrDefaultAsync(s => s.PlayerId == me && s.Key == "influencer:supported");
if (setting is null || !long.TryParse(setting.Value, out var influencerId)) return Content(
return Ok(new { Influencer = (object?)null }); setting is not null && long.TryParse(setting.Value, out var influencerId)
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == influencerId); ? ((int)influencerId).ToString()
return Ok(new { Influencer = player is null ? null : ToInfluencerWire(player) }); : "null",
"application/json");
} }
[HttpPost("api/influencerpartnerprogram/support")] [HttpPost("api/influencerpartnerprogram/support")]
@@ -86,7 +82,10 @@ public class InfluencerPartnerProgramController(DorkNetDbContext db) : Controlle
setting.Value = influencer.Id.ToString(); setting.Value = influencer.Id.ToString();
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Ok(new { Success = true, Influencer = ToInfluencerWire(influencer) }); // Client return type is LDGADANDBIO (the fire-and-forget POST response
// handle, same as PlayerCheer create) — it only checks success and
// ignores the body, so no typed shape is required here.
return Ok(new { Success = true, Influencer = (int)influencer.Id });
} }
[HttpPost("api/influencerpartnerprogram/remove")] [HttpPost("api/influencerpartnerprogram/remove")]
@@ -99,17 +98,4 @@ public class InfluencerPartnerProgramController(DorkNetDbContext db) : Controlle
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
return Ok(new { Success = true }); return Ok(new { Success = true });
} }
private static RecNetAccount ToInfluencerWire(PlayerEntity player) => new()
{
AccountId = (int)player.Id,
RawUsername = player.Username,
Username = player.Username,
DisplayName = player.DisplayName ?? player.Username,
ProfileImage = player.ProfileImageName ?? string.Empty,
TreatAsJunior = false,
HasBirthday = true,
Platforms = 1,
CreatedAt = player.CreatedAt == default ? DateTime.UtcNow : player.CreatedAt,
};
} }
@@ -2214,12 +2214,10 @@ public class AdminController(
await LogAsync("reset_avatar", "player", id, ""); await LogAsync("reset_avatar", "player", id, "");
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Push a profile update so the affected watch re-fetches its // No push: the affected client refetches its avatar on the next
// avatar without waiting for the next scene load. // scene load. The old SubscriptionUpdateProfile {Reason:"…"} blob
await notifications.NotifyAsync(id, // couldn't force an avatar refetch (wrong channel) and only decoded
PushNotificationId.SubscriptionUpdateProfile, // to a blank account ("Orphan player account (0)").
new { Reason = "AdminAvatarReset" });
return Ok(new return Ok(new
{ {
avatar.PlayerId, avatar.PlayerId,
@@ -2251,10 +2249,9 @@ public class AdminController(
await LogAsync("set_avatar", "player", id, body.OutfitSelections ?? ""); await LogAsync("set_avatar", "player", id, body.OutfitSelections ?? "");
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notifications.NotifyAsync(id, // No push: avatar changes propagate to other players in-room via
PushNotificationId.SubscriptionUpdateProfile, // Photon, and the affected client refetches on next scene load. The
new { Reason = "AdminAvatarUpdate" }); // old SubscriptionUpdateProfile blob only spawned a blank account.
return Ok(new { avatar.PlayerId, avatar.OutfitSelections, avatar.HairColor, avatar.SkinColor }); return Ok(new { avatar.PlayerId, avatar.OutfitSelections, avatar.HairColor, avatar.SkinColor });
} }
@@ -2291,10 +2288,9 @@ public class AdminController(
await LogAsync("grant_item", "player", id, $"{body.ItemId} x{qty}"); await LogAsync("grant_item", "player", id, $"{body.ItemId} x{qty}");
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notifications.NotifyAsync(id, // No push: the client shows the granted item on its next avatar
PushNotificationId.SubscriptionUpdateProfile, // fetch. The old SubscriptionUpdateProfile blob only spawned a
new { Reason = "InventoryGrant", body.ItemId, Quantity = qty }); // blank account ("Orphan player account (0)").
return Ok(new { granted = body.ItemId, quantity = qty }); return Ok(new { granted = body.ItemId, quantity = qty });
} }
@@ -2353,10 +2349,9 @@ public class AdminController(
$"cosmetics+{addedCosmetics} (catalog={catalog.Count}) slugs+{addedSlugs}"); $"cosmetics+{addedCosmetics} (catalog={catalog.Count}) slugs+{addedSlugs}");
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await notifications.NotifyAsync(id, // No push: the client shows the unlocked items on its next avatar
PushNotificationId.SubscriptionUpdateProfile, // fetch. The old SubscriptionUpdateProfile blob only spawned a
new { Reason = "AdminUnlockAll" }); // blank account ("Orphan player account (0)").
return Ok(new return Ok(new
{ {
id, id,
@@ -2447,10 +2442,10 @@ public class AdminController(
await LogAsync("set_level", "player", id, $"{previous} -> {newLevel}"); await LogAsync("set_level", "player", id, $"{previous} -> {newLevel}");
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// AccountChanged sends the correct full account. The old extra
// SubscriptionUpdateProfile {Reason:"AdminLevelSet",…} blob was a
// redundant blank-account push ("Orphan player account (0)").
await notifications.AccountChanged(p); await notifications.AccountChanged(p);
await notifications.NotifyAsync(id,
PushNotificationId.SubscriptionUpdateProfile,
new { Reason = "AdminLevelSet", Level = p.Level, XP = p.XP, PreviousLevel = previous });
return Ok(new { p.Id, p.Level, p.XP }); return Ok(new { p.Id, p.Level, p.XP });
} }
@@ -2497,12 +2492,9 @@ public class AdminController(
$"inserted={missing.Count} types=[{string.Join(",", missing)}]"); $"inserted={missing.Count} types=[{string.Join(",", missing)}]");
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Nudge the watch to re-fetch its cheer list so the pinned-cheer // No push: the pinned-cheer picker refetches the cheer list on next
// picker shows the newly-unlocked badges without a relog. // open. The old SubscriptionUpdateProfile blob only spawned a blank
await notifications.NotifyAsync(id, // account ("Orphan player account (0)").
PushNotificationId.SubscriptionUpdateProfile,
new { Reason = "AdminCheerUnlock", Inserted = missing.Count });
return Ok(new { id, inserted = missing.Count, types = missing }); return Ok(new { id, inserted = missing.Count, types = missing });
} }