Build & Deploy / build (push) Successful in 53s
The friend tile rendered empty/offline because the UE client couldn't read our best-guess response shape. Expose each friend's identity/name/steam/presence under every plausible field name (camelCase + PlayFab PascalCase, flat + nested profile) under both friends/Friends keys, and log the JSON so we can pin the exact shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.6 KiB
C#
120 lines
5.6 KiB
C#
using System.Text.Json;
|
|
using Prospect.Server.Api.Services.Auth.Extensions;
|
|
using Prospect.Server.Api.Services.CloudScript.Models;
|
|
using Prospect.Server.Api.Services.Database;
|
|
using Prospect.Server.Api.Services.Database.Models;
|
|
using Prospect.Server.Api.Services.UserData;
|
|
|
|
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
|
|
|
[CloudScriptFunction("GetFriendList")]
|
|
public class GetFriendList : ICloudScriptFunction<FYBaseSocialRequest, object?>
|
|
{
|
|
private readonly ILogger<GetFriendList> _logger;
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
private readonly UserDataService _userDataService;
|
|
private readonly DbUserService _userService;
|
|
|
|
public GetFriendList(ILogger<GetFriendList> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, DbUserService userService)
|
|
{
|
|
_logger = logger;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_userDataService = userDataService;
|
|
_userService = userService;
|
|
}
|
|
|
|
public async Task<object?> ExecuteAsync(FYBaseSocialRequest request)
|
|
{
|
|
var context = _httpContextAccessor.HttpContext;
|
|
if (context == null)
|
|
{
|
|
throw new CloudScriptException("CloudScript was not called within a http request");
|
|
}
|
|
var userId = context.User.FindAuthUserId();
|
|
|
|
// Imported Steam friends (persisted by ClientsideFriendsImport), resolved to players
|
|
// that actually exist on this private server.
|
|
var steamIds = new List<string>();
|
|
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "ImportedSteamFriends" });
|
|
if (userData.TryGetValue("ImportedSteamFriends", out var rec) && !string.IsNullOrWhiteSpace(rec.Value))
|
|
{
|
|
try { steamIds = JsonSerializer.Deserialize<List<string>>(rec.Value) ?? new List<string>(); }
|
|
catch { /* ignore malformed */ }
|
|
}
|
|
|
|
var friends = new List<object>();
|
|
if (steamIds.Count > 0)
|
|
{
|
|
var players = await _userService.FindManyAsync(PlayFabUserAuthType.Steam, steamIds);
|
|
var nowUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
foreach (var player in players)
|
|
{
|
|
if (player.Id == userId) continue; // never list yourself
|
|
|
|
var steamId = player.Auth?.FirstOrDefault(a => a.Type == PlayFabUserAuthType.Steam)?.Key ?? "";
|
|
var displayName = string.IsNullOrWhiteSpace(player.DisplayName) ? "Prospector" : player.DisplayName;
|
|
|
|
// 0 = offline, 1 = online, 2 = in match. Online if seen in the last 3 minutes.
|
|
var onlineState = 0;
|
|
var presenceData = await _userDataService.FindAsync(player.Id, player.Id, new List<string> { "PresenceState" });
|
|
if (presenceData.TryGetValue("PresenceState", out var presenceRec) && !string.IsNullOrWhiteSpace(presenceRec.Value))
|
|
{
|
|
try
|
|
{
|
|
var presence = JsonSerializer.Deserialize<PlayerPresenceState>(presenceRec.Value);
|
|
if (presence != null && nowUtc - presence.LastSeenUtc <= 180)
|
|
{
|
|
onlineState = presence.InMatch ? 2 : 1;
|
|
}
|
|
}
|
|
catch { /* ignore malformed presence */ }
|
|
}
|
|
|
|
// The exact model the UE client binds is not documented, so expose the identity
|
|
// under every plausible field name (camelCase + PlayFab PascalCase) and both a
|
|
// flat and nested profile — whichever the client reads, the tile gets populated.
|
|
friends.Add(new
|
|
{
|
|
// identity
|
|
friendPlayFabId = player.Id,
|
|
FriendPlayFabId = player.Id,
|
|
playerId = player.Id,
|
|
PlayerId = player.Id,
|
|
playFabId = player.Id,
|
|
PlayFabId = player.Id,
|
|
// name
|
|
displayName,
|
|
DisplayName = displayName,
|
|
titleDisplayName = displayName,
|
|
TitleDisplayName = displayName,
|
|
name = displayName,
|
|
username = displayName,
|
|
Username = displayName,
|
|
// steam
|
|
steamId,
|
|
SteamId = steamId,
|
|
steamInfo = new { steamId, SteamId = steamId },
|
|
SteamInfo = new { SteamId = steamId, steamId },
|
|
// presence
|
|
onlineState,
|
|
OnlineState = onlineState,
|
|
isOnline = onlineState > 0,
|
|
inMatch = onlineState == 2,
|
|
presence = new { onlineState, state = onlineState },
|
|
avatarUrl = "",
|
|
// nested profiles
|
|
profile = new { playerId = player.Id, displayName, avatarUrl = "" },
|
|
Profile = new { PlayerId = player.Id, DisplayName = displayName, PlayerProfileModel = new { DisplayName = displayName } },
|
|
tags = Array.Empty<string>(),
|
|
Tags = Array.Empty<string>(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Return the list under both the camelCase and PlayFab-cased container keys.
|
|
var result = new { friends, Friends = friends, count = friends.Count };
|
|
_logger.LogInformation("GetFriendList for {User}: {Count} friend(s); payload={Json}", userId, friends.Count, JsonSerializer.Serialize(result));
|
|
return result;
|
|
}
|
|
}
|