feat: GET /api/v1/members/{id}, api v1 flags

This commit is contained in:
sam 2024-12-25 14:23:16 -05:00
parent 2281b3e478
commit d182b07482
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
6 changed files with 229 additions and 22 deletions

View file

@ -4,13 +4,36 @@ using Microsoft.AspNetCore.Mvc;
namespace Foxnouns.Backend.Controllers.V1; namespace Foxnouns.Backend.Controllers.V1;
[Route("/api/v1/users")] [Route("/api/v1")]
public class UsersV1Controller(UsersV1Service usersV1Service) : ApiControllerBase public class UsersV1Controller(UsersV1Service usersV1Service, MembersV1Service membersV1Service)
: ApiControllerBase
{ {
[HttpGet("{userRef}")] [HttpGet("users/{userRef}")]
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default) public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
{ {
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
return Ok(await usersV1Service.RenderUserAsync(user)); return Ok(
await usersV1Service.RenderUserAsync(
user,
CurrentToken,
renderMembers: true,
renderFlags: true,
ct: ct
)
);
}
[HttpGet("members/{id}")]
public async Task<IActionResult> GetMemberAsync(string id, CancellationToken ct = default)
{
Member member = await membersV1Service.ResolveMemberAsync(id, ct);
return Ok(
await membersV1Service.RenderMemberAsync(
member,
CurrentToken,
renderFlags: true,
ct: ct
)
);
} }
} }

View file

@ -0,0 +1,44 @@
// ReSharper disable NotAccessedPositionalProperty.Global
using Foxnouns.Backend.Database;
using Newtonsoft.Json;
namespace Foxnouns.Backend.Dto.V1;
public record PartialMember(
string Id,
Snowflake IdNew,
string Sid,
string Name,
string? DisplayName,
string? Bio,
string? Avatar,
string[] Links,
FieldEntry[] Names,
PronounEntry[] Pronouns
);
public record MemberResponse(
string Id,
Snowflake IdNew,
string Sid,
string Name,
string? DisplayName,
string? Bio,
string? Avatar,
string[] Links,
FieldEntry[] Names,
PronounEntry[] Pronouns,
ProfileField[] Fields,
PrideFlag[] Flags,
PartialUser User,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted
);
public record PartialUser(
string Id,
Snowflake IdNew,
string Name,
string? DisplayName,
string? Avatar,
Dictionary<Guid, CustomPreference> CustomPreferences
);

View file

@ -21,6 +21,8 @@ public record UserResponse(
FieldEntry[] Names, FieldEntry[] Names,
PronounEntry[] Pronouns, PronounEntry[] Pronouns,
ProfileField[] Fields, ProfileField[] Fields,
PrideFlag[] Flags,
PartialMember[] Members,
int? UtcOffset, int? UtcOffset,
Dictionary<Guid, CustomPreference> CustomPreferences Dictionary<Guid, CustomPreference> CustomPreferences
); );
@ -75,3 +77,5 @@ public record PronounEntry(string Pronouns, string? DisplayText, string Status)
)) ))
.ToArray(); .ToArray();
} }
public record PrideFlag(string Id, Snowflake IdNew, string Hash, string Name, string? Description);

View file

@ -130,7 +130,8 @@ public static class WebApplicationExtensions
.AddTransient<CreateFlagInvocable>() .AddTransient<CreateFlagInvocable>()
.AddTransient<CreateDataExportInvocable>() .AddTransient<CreateDataExportInvocable>()
// Legacy services // Legacy services
.AddScoped<UsersV1Service>(); .AddScoped<UsersV1Service>()
.AddScoped<MembersV1Service>();
if (!config.Logging.EnableMetrics) if (!config.Logging.EnableMetrics)
services.AddHostedService<BackgroundMetricsCollectionService>(); services.AddHostedService<BackgroundMetricsCollectionService>();

View file

@ -0,0 +1,72 @@
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto.V1;
using Microsoft.EntityFrameworkCore;
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
namespace Foxnouns.Backend.Services.V1;
public class MembersV1Service(DatabaseContext db)
{
public async Task<Member> ResolveMemberAsync(string id, CancellationToken ct = default)
{
Member? member;
if (Snowflake.TryParse(id, out Snowflake? sf))
{
member = await db
.Members.Include(m => m.User)
.FirstOrDefaultAsync(m => m.Id == sf && !m.User.Deleted, ct);
if (member != null)
return member;
}
member = await db
.Members.Include(m => m.User)
.FirstOrDefaultAsync(m => m.LegacyId == id && !m.User.Deleted, ct);
if (member != null)
return member;
throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound);
}
public async Task<MemberResponse> RenderMemberAsync(
Member m,
Token? token = default,
bool renderFlags = true,
CancellationToken ct = default
)
{
bool renderUnlisted = m.UserId == token?.UserId;
List<MemberFlag> flags = renderFlags
? await db.MemberFlags.Where(f => f.MemberId == m.Id).OrderBy(f => f.Id).ToListAsync(ct)
: [];
return new MemberResponse(
m.LegacyId,
m.Id,
m.Sid,
m.Name,
m.DisplayName,
m.Bio,
m.Avatar,
m.Links,
Names: FieldEntry.FromEntries(m.Names, m.User.CustomPreferences),
Pronouns: PronounEntry.FromPronouns(m.Pronouns, m.User.CustomPreferences),
Fields: ProfileField.FromFields(m.Fields, m.User.CustomPreferences),
Flags: flags
.Where(f => f.PrideFlag.Hash != null)
.Select(f => new PrideFlag(
f.PrideFlag.LegacyId,
f.PrideFlag.Id,
f.PrideFlag.Hash!,
f.PrideFlag.Name,
f.PrideFlag.Description
))
.ToArray(),
User: UsersV1Service.RenderPartialUser(m.User),
Unlisted: renderUnlisted ? m.Unlisted : null
);
}
}

View file

@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto.V1; using Foxnouns.Backend.Dto.V1;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry; using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
namespace Foxnouns.Backend.Services.V1; namespace Foxnouns.Backend.Services.V1;
@ -49,8 +50,26 @@ public class UsersV1Service(DatabaseContext db)
); );
} }
public async Task<UserResponse> RenderUserAsync(User user) public async Task<UserResponse> RenderUserAsync(
User user,
Token? token = null,
bool renderMembers = true,
bool renderFlags = true,
CancellationToken ct = default
)
{ {
bool isSelfUser = user.Id == token?.UserId;
renderMembers = renderMembers && (isSelfUser || !user.ListHidden);
// Only fetch members if we're rendering members (duh)
List<Member> members = renderMembers
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
: [];
List<UserFlag> flags = renderFlags
? await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct)
: [];
int? utcOffset = null; int? utcOffset = null;
if ( if (
user.Timezone != null user.Timezone != null
@ -70,11 +89,30 @@ public class UsersV1Service(DatabaseContext db)
user.MemberTitle, user.MemberTitle,
user.Avatar, user.Avatar,
user.Links, user.Links,
FieldEntry.FromEntries(user.Names, user.CustomPreferences), Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences),
PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences), Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
ProfileField.FromFields(user.Fields, user.CustomPreferences), Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences),
Flags: flags
.Where(f => f.PrideFlag.Hash != null)
.Select(f => new PrideFlag(
f.PrideFlag.LegacyId,
f.PrideFlag.Id,
f.PrideFlag.Hash!,
f.PrideFlag.Name,
f.PrideFlag.Description
))
.ToArray(),
Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(),
utcOffset, utcOffset,
user.CustomPreferences.Select(x => CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
);
}
private static Dictionary<Guid, CustomPreference> RenderCustomPreferences(
Dictionary<Snowflake, User.CustomPreference> customPreferences
) =>
customPreferences
.Select(x =>
( (
x.Value.LegacyId, x.Value.LegacyId,
new CustomPreference( new CustomPreference(
@ -86,7 +124,32 @@ public class UsersV1Service(DatabaseContext db)
) )
) )
) )
.ToDictionary() .ToDictionary();
private static PartialMember RenderPartialMember(
Member m,
Dictionary<Snowflake, User.CustomPreference> customPreferences
) =>
new(
m.LegacyId,
m.Id,
m.Sid,
m.Name,
m.DisplayName,
m.Bio,
m.Avatar,
m.Links,
Names: FieldEntry.FromEntries(m.Names, customPreferences),
Pronouns: PronounEntry.FromPronouns(m.Pronouns, customPreferences)
);
public static PartialUser RenderPartialUser(User user) =>
new(
user.LegacyId,
user.Id,
user.Username,
user.DisplayName,
user.Avatar,
CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
); );
}
} }