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 { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly UserDataService _userDataService; private readonly DbUserService _userService; public GetFriendList(ILogger logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, DbUserService userService) { _logger = logger; _httpContextAccessor = httpContextAccessor; _userDataService = userDataService; _userService = userService; } public async Task 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(); var userData = await _userDataService.FindAsync(userId, userId, new List { "ImportedSteamFriends" }); if (userData.TryGetValue("ImportedSteamFriends", out var rec) && !string.IsNullOrWhiteSpace(rec.Value)) { try { steamIds = JsonSerializer.Deserialize>(rec.Value) ?? new List(); } catch { /* ignore malformed */ } } var friends = new List(); 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 { "PresenceState" }); if (presenceData.TryGetValue("PresenceState", out var presenceRec) && !string.IsNullOrWhiteSpace(presenceRec.Value)) { try { var presence = JsonSerializer.Deserialize(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(), Tags = Array.Empty(), }); } } // 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; } }