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;
[Route("/api/v1/users")]
public class UsersV1Controller(UsersV1Service usersV1Service) : ApiControllerBase
[Route("/api/v1")]
public class UsersV1Controller(UsersV1Service usersV1Service, MembersV1Service membersV1Service)
: ApiControllerBase
{
[HttpGet("{userRef}")]
[HttpGet("users/{userRef}")]
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
{
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,
PronounEntry[] Pronouns,
ProfileField[] Fields,
PrideFlag[] Flags,
PartialMember[] Members,
int? UtcOffset,
Dictionary<Guid, CustomPreference> CustomPreferences
);
@ -75,3 +77,5 @@ public record PronounEntry(string Pronouns, string? DisplayText, string Status)
))
.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<CreateDataExportInvocable>()
// Legacy services
.AddScoped<UsersV1Service>();
.AddScoped<UsersV1Service>()
.AddScoped<MembersV1Service>();
if (!config.Logging.EnableMetrics)
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 Microsoft.EntityFrameworkCore;
using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry;
using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag;
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;
if (
user.Timezone != null
@ -70,23 +89,67 @@ public class UsersV1Service(DatabaseContext db)
user.MemberTitle,
user.Avatar,
user.Links,
FieldEntry.FromEntries(user.Names, user.CustomPreferences),
PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
ProfileField.FromFields(user.Fields, user.CustomPreferences),
Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences),
Pronouns: PronounEntry.FromPronouns(user.Pronouns, 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,
user.CustomPreferences.Select(x =>
(
x.Value.LegacyId,
new CustomPreference(
x.Value.Icon,
x.Value.Tooltip,
x.Value.Size,
x.Value.Muted,
x.Value.Favourite
)
)
)
.ToDictionary()
CustomPreferences: RenderCustomPreferences(user.CustomPreferences)
);
}
private static Dictionary<Guid, CustomPreference> RenderCustomPreferences(
Dictionary<Snowflake, User.CustomPreference> customPreferences
) =>
customPreferences
.Select(x =>
(
x.Value.LegacyId,
new CustomPreference(
x.Value.Icon,
x.Value.Tooltip,
x.Value.Size,
x.Value.Muted,
x.Value.Favourite
)
)
)
.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)
);
}