feat(social): friends list resolution + fix ping request 500
Build & Deploy / build (push) Successful in 24s
Build & Deploy / build (push) Successful in 24s
- RequestUsersPingRequest: the client sends ping=2147483648 (int.MaxValue+1) for "no ping", which overflowed the int field and made every ping request return 500. Widened to long. - ClientsideFriendsImport: persist the imported Steam friend ids per user. - GetFriendList: resolve those ids to the players that exist on this server (batch lookup by Steam auth key) and return them as friends. Response shape is best-effort pending in-game confirmation. - DbUserService.FindManyAsync: batch-resolve auth keys to players. First step of the station lobby (see friends on the server). Squad formation / invite flow to follow once friends are visible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Prospect.Server.Api.Services.Auth.Extensions;
|
||||
using Prospect.Server.Api.Services.CloudScript;
|
||||
using Prospect.Server.Api.Services.UserData;
|
||||
|
||||
public class ClientsideFriendsImportRequest
|
||||
{
|
||||
@@ -18,15 +21,33 @@ public class ClientsideFriendsImportResponse
|
||||
public class ClientsideFriendsImportFunction : ICloudScriptFunction<ClientsideFriendsImportRequest, ClientsideFriendsImportResponse>
|
||||
{
|
||||
private readonly ILogger<ClientsideFriendsImportFunction> _logger;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly UserDataService _userDataService;
|
||||
|
||||
public ClientsideFriendsImportFunction(ILogger<ClientsideFriendsImportFunction> logger)
|
||||
public ClientsideFriendsImportFunction(ILogger<ClientsideFriendsImportFunction> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_userDataService = userDataService;
|
||||
}
|
||||
|
||||
public async Task<ClientsideFriendsImportResponse> ExecuteAsync(ClientsideFriendsImportRequest request)
|
||||
{
|
||||
_logger.LogInformation("Processing friends import");
|
||||
var context = _httpContextAccessor.HttpContext;
|
||||
if (context == null)
|
||||
{
|
||||
throw new CloudScriptException("CloudScript was not called within a http request");
|
||||
}
|
||||
var userId = context.User.FindAuthUserId();
|
||||
|
||||
// Persist the platform friend ids the client imports (Steam ids) so GetFriendList
|
||||
// can later resolve which of them actually play on this server.
|
||||
var ids = request.UserIDs ?? Array.Empty<string>();
|
||||
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>
|
||||
{
|
||||
["ImportedSteamFriends"] = JsonSerializer.Serialize(ids),
|
||||
});
|
||||
_logger.LogInformation("Imported {Count} {Platform} friend id(s) for user {User}", ids.Length, request.Platform, userId);
|
||||
|
||||
return new ClientsideFriendsImportResponse{};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,71 @@
|
||||
using Prospect.Server.Api.Services.CloudScript.Models;
|
||||
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?>
|
||||
{
|
||||
public Task<object?> ExecuteAsync(FYBaseSocialRequest request)
|
||||
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)
|
||||
{
|
||||
return Task.FromResult<object?>(new
|
||||
{
|
||||
|
||||
});
|
||||
_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();
|
||||
|
||||
// Resolve the imported Steam friends (persisted by ClientsideFriendsImport) to the
|
||||
// players that actually exist on this server. It's a private server, so only friends
|
||||
// who have logged in here will show up.
|
||||
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);
|
||||
foreach (var player in players)
|
||||
{
|
||||
if (player.Id == userId) continue; // never list yourself
|
||||
friends.Add(new
|
||||
{
|
||||
profile = new
|
||||
{
|
||||
playerId = player.Id,
|
||||
displayName = player.DisplayName,
|
||||
avatarUrl = "",
|
||||
},
|
||||
onlineState = 0, // EYUserState — real presence tracking is a later phase
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the exact response shape the client expects is not yet confirmed; this is a
|
||||
// best-effort structure. The log lets us verify resolution while we validate in game.
|
||||
_logger.LogInformation("GetFriendList for {User}: {Count} friend(s) resolved on server", userId, friends.Count);
|
||||
return new { friends };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ public class RegionObject
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; set; }
|
||||
[JsonPropertyName("ping")]
|
||||
public int Ping { get; set; }
|
||||
public long Ping { get; set; } // client sends 2147483648 (int.MaxValue+1) for "no ping" -> must be long or deserialization 500s
|
||||
[JsonPropertyName("instanceType")]
|
||||
public int InstanceType { get; set; }
|
||||
}
|
||||
|
||||
@@ -40,7 +40,21 @@ public class DbUserService : BaseDbService<PlayFabUser>
|
||||
|
||||
public async Task<PlayFabUser> FindOrCreateAsync(PlayFabUserAuthType type, string key)
|
||||
{
|
||||
return await FindAsync(type, key) ??
|
||||
return await FindAsync(type, key) ??
|
||||
await CreateAsync(type, key);
|
||||
}
|
||||
|
||||
// Resolve several auth keys (e.g. a batch of Steam IDs) to the players that exist on
|
||||
// this server in a single query. Used by the friends list to keep only the imported
|
||||
// Steam friends who have actually played here.
|
||||
public async Task<List<PlayFabUser>> FindManyAsync(PlayFabUserAuthType type, IReadOnlyCollection<string> keys)
|
||||
{
|
||||
if (keys == null || keys.Count == 0)
|
||||
{
|
||||
return new List<PlayFabUser>();
|
||||
}
|
||||
var wanted = new HashSet<string>(keys);
|
||||
return await Collection.Find(user => user.Auth.Any(auth =>
|
||||
auth.Type == type && wanted.Contains(auth.Key))).ToListAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user