chore: add csharpier to husky, format backend with csharpier

This commit is contained in:
sam 2024-10-02 00:28:07 +02:00
parent 5fab66444f
commit 7f971e8549
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
73 changed files with 2098 additions and 1048 deletions

View file

@ -16,8 +16,12 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
/// Creates a new user with the given email address and password.
/// This method does <i>not</i> save the resulting user, the caller must still call <see cref="M:Microsoft.EntityFrameworkCore.DbContext.SaveChanges" />.
/// </summary>
public async Task<User> CreateUserWithPasswordAsync(string username, string email, string password,
CancellationToken ct = default)
public async Task<User> CreateUserWithPasswordAsync(
string username,
string email,
string password,
CancellationToken ct = default
)
{
var user = new User
{
@ -26,9 +30,13 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
AuthMethods =
{
new AuthMethod
{ Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email }
{
Id = snowflakeGenerator.GenerateSnowflake(),
AuthType = AuthType.Email,
RemoteId = email,
},
},
LastActive = clock.GetCurrentInstant()
LastActive = clock.GetCurrentInstant(),
};
db.Add(user);
@ -42,8 +50,14 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
/// To create a user with email authentication, use <see cref="CreateUserWithPasswordAsync" />
/// This method does <i>not</i> save the resulting user, the caller must still call <see cref="M:Microsoft.EntityFrameworkCore.DbContext.SaveChanges" />.
/// </summary>
public async Task<User> CreateUserWithRemoteAuthAsync(string username, AuthType authType, string remoteId,
string remoteUsername, FediverseApplication? instance = null, CancellationToken ct = default)
public async Task<User> CreateUserWithRemoteAuthAsync(
string username,
AuthType authType,
string remoteId,
string remoteUsername,
FediverseApplication? instance = null,
CancellationToken ct = default
)
{
AssertValidAuthType(authType, instance);
@ -58,11 +72,14 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
{
new AuthMethod
{
Id = snowflakeGenerator.GenerateSnowflake(), AuthType = authType, RemoteId = remoteId,
RemoteUsername = remoteUsername, FediverseApplication = instance
}
Id = snowflakeGenerator.GenerateSnowflake(),
AuthType = authType,
RemoteId = remoteId,
RemoteUsername = remoteUsername,
FediverseApplication = instance,
},
},
LastActive = clock.GetCurrentInstant()
LastActive = clock.GetCurrentInstant(),
};
db.Add(user);
@ -78,19 +95,31 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
/// <returns>A tuple of the authenticated user and whether multi-factor authentication is required</returns>
/// <exception cref="ApiError.NotFound">Thrown if the email address is not associated with any user
/// or if the password is incorrect</exception>
public async Task<(User, EmailAuthenticationResult)> AuthenticateUserAsync(string email, string password,
CancellationToken ct = default)
public async Task<(User, EmailAuthenticationResult)> AuthenticateUserAsync(
string email,
string password,
CancellationToken ct = default
)
{
var user = await db.Users.FirstOrDefaultAsync(u =>
u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email), ct);
var user = await db.Users.FirstOrDefaultAsync(
u => u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email),
ct
);
if (user == null)
throw new ApiError.NotFound("No user with that email address found, or password is incorrect",
ErrorCode.UserNotFound);
throw new ApiError.NotFound(
"No user with that email address found, or password is incorrect",
ErrorCode.UserNotFound
);
var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), ct);
var pwResult = await Task.Run(
() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password),
ct
);
if (pwResult == PasswordVerificationResult.Failed) // TODO: this seems to fail on some valid passwords?
throw new ApiError.NotFound("No user with that email address found, or password is incorrect",
ErrorCode.UserNotFound);
throw new ApiError.NotFound(
"No user with that email address found, or password is incorrect",
ErrorCode.UserNotFound
);
if (pwResult == PasswordVerificationResult.SuccessRehashNeeded)
{
user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct);
@ -117,19 +146,33 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
/// <returns>A user object, or null if the remote account isn't linked to any user.</returns>
/// <exception cref="FoxnounsError">Thrown if <c>instance</c> is passed when not required,
/// or not passed when required</exception>
public async Task<User?> AuthenticateUserAsync(AuthType authType, string remoteId,
FediverseApplication? instance = null, CancellationToken ct = default)
public async Task<User?> AuthenticateUserAsync(
AuthType authType,
string remoteId,
FediverseApplication? instance = null,
CancellationToken ct = default
)
{
AssertValidAuthType(authType, instance);
return await db.Users.FirstOrDefaultAsync(u =>
u.AuthMethods.Any(a =>
a.AuthType == authType && a.RemoteId == remoteId && a.FediverseApplication == instance), ct);
return await db.Users.FirstOrDefaultAsync(
u =>
u.AuthMethods.Any(a =>
a.AuthType == authType
&& a.RemoteId == remoteId
&& a.FediverseApplication == instance
),
ct
);
}
public async Task<AuthMethod> AddAuthMethodAsync(Snowflake userId, AuthType authType, string remoteId,
public async Task<AuthMethod> AddAuthMethodAsync(
Snowflake userId,
AuthType authType,
string remoteId,
string? remoteUsername = null,
CancellationToken ct = default)
CancellationToken ct = default
)
{
AssertValidAuthType(authType, null);
@ -139,7 +182,7 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
AuthType = authType,
RemoteId = remoteId,
RemoteUsername = remoteUsername,
UserId = userId
UserId = userId,
};
db.Add(authMethod);
@ -147,21 +190,33 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
return authMethod;
}
public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires)
public (string, Token) GenerateToken(
User user,
Application application,
string[] scopes,
Instant expires
)
{
if (!AuthUtils.ValidateScopes(application, scopes))
throw new ApiError.BadRequest("Invalid scopes requested for this token", "scopes", scopes);
throw new ApiError.BadRequest(
"Invalid scopes requested for this token",
"scopes",
scopes
);
var (token, hash) = GenerateToken();
return (token, new Token
{
Id = snowflakeGenerator.GenerateSnowflake(),
Hash = hash,
Application = application,
User = user,
ExpiresAt = expires,
Scopes = scopes
});
return (
token,
new Token
{
Id = snowflakeGenerator.GenerateSnowflake(),
Hash = hash,
Application = application,
User = user,
ExpiresAt = expires,
Scopes = scopes,
}
);
}
private static (string, byte[]) GenerateToken()
@ -179,4 +234,4 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
if (authType != AuthType.Fediverse && instance != null)
throw new FoxnounsError("Non-Fediverse authentication does not require an instance.");
}
}
}

View file

@ -10,26 +10,43 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
{
private readonly ILogger _logger = logger.ForContext<KeyCacheService>();
public Task SetKeyAsync(string key, string value, Duration expireAfter, CancellationToken ct = default) =>
SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter, ct);
public Task SetKeyAsync(
string key,
string value,
Duration expireAfter,
CancellationToken ct = default
) => SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter, ct);
public async Task SetKeyAsync(string key, string value, Instant expires, CancellationToken ct = default)
public async Task SetKeyAsync(
string key,
string value,
Instant expires,
CancellationToken ct = default
)
{
db.TemporaryKeys.Add(new TemporaryKey
{
Expires = expires,
Key = key,
Value = value,
});
db.TemporaryKeys.Add(
new TemporaryKey
{
Expires = expires,
Key = key,
Value = value,
}
);
await db.SaveChangesAsync(ct);
}
public async Task<string?> GetKeyAsync(string key, bool delete = false, CancellationToken ct = default)
public async Task<string?> GetKeyAsync(
string key,
bool delete = false,
CancellationToken ct = default
)
{
var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct);
if (value == null) return null;
if (value == null)
return null;
if (delete) await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct);
if (delete)
await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct);
return value.Value;
}
@ -39,23 +56,41 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger)
public async Task DeleteExpiredKeysAsync(CancellationToken ct)
{
var count = await db.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()).ExecuteDeleteAsync(ct);
if (count != 0) _logger.Information("Removed {Count} expired keys from the database", count);
var count = await db
.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant())
.ExecuteDeleteAsync(ct);
if (count != 0)
_logger.Information("Removed {Count} expired keys from the database", count);
}
public Task SetKeyAsync<T>(string key, T obj, Duration expiresAt, CancellationToken ct = default) where T : class =>
SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt, ct);
public Task SetKeyAsync<T>(
string key,
T obj,
Duration expiresAt,
CancellationToken ct = default
)
where T : class => SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt, ct);
public async Task SetKeyAsync<T>(string key, T obj, Instant expires, CancellationToken ct = default) where T : class
public async Task SetKeyAsync<T>(
string key,
T obj,
Instant expires,
CancellationToken ct = default
)
where T : class
{
var value = JsonConvert.SerializeObject(obj);
await SetKeyAsync(key, value, expires, ct);
}
public async Task<T?> GetKeyAsync<T>(string key, bool delete = false, CancellationToken ct = default)
public async Task<T?> GetKeyAsync<T>(
string key,
bool delete = false,
CancellationToken ct = default
)
where T : class
{
var value = await GetKeyAsync(key, delete, ct);
return value == null ? default : JsonConvert.DeserializeObject<T>(value);
}
}
}

View file

@ -15,12 +15,17 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co
_logger.Debug("Sending account creation email to {ToEmail}", to);
try
{
await mailer.SendAsync(new AccountCreationMailable(config, new AccountCreationMailableView
{
BaseUrl = config.BaseUrl,
To = to,
Code = code
}));
await mailer.SendAsync(
new AccountCreationMailable(
config,
new AccountCreationMailableView
{
BaseUrl = config.BaseUrl,
To = to,
Code = code,
}
)
);
}
catch (Exception exc)
{
@ -28,4 +33,4 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co
}
});
}
}
}

View file

@ -10,17 +10,17 @@ public class MemberRendererService(DatabaseContext db, Config config)
{
public async Task<IEnumerable<PartialMember>> RenderUserMembersAsync(User user, Token? token)
{
var canReadHiddenMembers = token != null && token.UserId == user.Id && token.HasScope("member.read");
var renderUnlisted = token != null && token.UserId == user.Id && token.HasScope("user.read_hidden");
var canReadHiddenMembers =
token != null && token.UserId == user.Id && token.HasScope("member.read");
var renderUnlisted =
token != null && token.UserId == user.Id && token.HasScope("user.read_hidden");
var canReadMemberList = !user.ListHidden || canReadHiddenMembers;
IEnumerable<Member> members = canReadMemberList
? await db.Members
.Where(m => m.UserId == user.Id)
.OrderBy(m => m.Name)
.ToListAsync()
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync()
: [];
if (!canReadHiddenMembers) members = members.Where(m => !m.Unlisted);
if (!canReadHiddenMembers)
members = members.Where(m => !m.Unlisted);
return members.Select(m => RenderPartialMember(m, renderUnlisted));
}
@ -29,25 +29,54 @@ public class MemberRendererService(DatabaseContext db, Config config)
var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden");
return new MemberResponse(
member.Id, member.Sid, member.Name, member.DisplayName, member.Bio,
AvatarUrlFor(member), member.Links, member.Names, member.Pronouns, member.Fields,
member.Id,
member.Sid,
member.Name,
member.DisplayName,
member.Bio,
AvatarUrlFor(member),
member.Links,
member.Names,
member.Pronouns,
member.Fields,
member.ProfileFlags.Select(f => RenderPrideFlag(f.PrideFlag)),
RenderPartialUser(member.User), renderUnlisted ? member.Unlisted : null);
RenderPartialUser(member.User),
renderUnlisted ? member.Unlisted : null
);
}
private UserRendererService.PartialUser RenderPartialUser(User user) =>
new(user.Id, user.Sid, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences);
new(
user.Id,
user.Sid,
user.Username,
user.DisplayName,
AvatarUrlFor(user),
user.CustomPreferences
);
public PartialMember RenderPartialMember(Member member, bool renderUnlisted = false) => new(member.Id, member.Sid,
member.Name,
member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns,
renderUnlisted ? member.Unlisted : null);
public PartialMember RenderPartialMember(Member member, bool renderUnlisted = false) =>
new(
member.Id,
member.Sid,
member.Name,
member.DisplayName,
member.Bio,
AvatarUrlFor(member),
member.Names,
member.Pronouns,
renderUnlisted ? member.Unlisted : null
);
private string? AvatarUrlFor(Member member) =>
member.Avatar != null ? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp" : null;
member.Avatar != null
? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp"
: null;
private string? AvatarUrlFor(User user) =>
user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null;
user.Avatar != null
? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp"
: null;
private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp";
@ -63,8 +92,8 @@ public class MemberRendererService(DatabaseContext db, Config config)
string? AvatarUrl,
IEnumerable<FieldEntry> Names,
IEnumerable<Pronoun> Pronouns,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
bool? Unlisted);
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted
);
public record MemberResponse(
Snowflake Id,
@ -79,6 +108,6 @@ public class MemberRendererService(DatabaseContext db, Config config)
IEnumerable<Field> Fields,
IEnumerable<UserRendererService.PrideFlagResponse> Flags,
UserRendererService.PartialUser User,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
bool? Unlisted);
}
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted
);
}

View file

@ -6,10 +6,7 @@ using Prometheus;
namespace Foxnouns.Backend.Services;
public class MetricsCollectionService(
ILogger logger,
IServiceProvider services,
IClock clock)
public class MetricsCollectionService(ILogger logger, IServiceProvider services, IClock clock)
{
private readonly ILogger _logger = logger.ForContext<MetricsCollectionService>();
@ -31,8 +28,10 @@ public class MetricsCollectionService(
FoxnounsMetrics.UsersActiveWeekCount.Set(users.Count(i => i > now - Week));
FoxnounsMetrics.UsersActiveDayCount.Set(users.Count(i => i > now - Day));
var memberCount = await db.Members.Include(m => m.User)
.Where(m => !m.Unlisted && !m.User.ListHidden && !m.User.Deleted).CountAsync(ct);
var memberCount = await db
.Members.Include(m => m.User)
.Where(m => !m.Unlisted && !m.User.ListHidden && !m.User.Deleted)
.CountAsync(ct);
FoxnounsMetrics.MemberCount.Set(memberCount);
var process = Process.GetCurrentProcess();
@ -42,13 +41,17 @@ public class MetricsCollectionService(
FoxnounsMetrics.ProcessThreads.Set(process.Threads.Count);
FoxnounsMetrics.ProcessHandles.Set(process.HandleCount);
_logger.Information("Collected metrics in {DurationMilliseconds} ms",
timer.ObserveDuration().TotalMilliseconds);
_logger.Information(
"Collected metrics in {DurationMilliseconds} ms",
timer.ObserveDuration().TotalMilliseconds
);
}
}
public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectionService metricsCollectionService)
: BackgroundService
public class BackgroundMetricsCollectionService(
ILogger logger,
MetricsCollectionService metricsCollectionService
) : BackgroundService
{
private readonly ILogger _logger = logger.ForContext<BackgroundMetricsCollectionService>();
@ -63,4 +66,4 @@ public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectio
await metricsCollectionService.CollectMetricsAsync(ct);
}
}
}
}

View file

@ -15,7 +15,8 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi
{
await minioClient.RemoveObjectAsync(
new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path),
ct);
ct
);
}
catch (InvalidObjectNameException)
{
@ -23,17 +24,28 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi
}
}
public async Task PutObjectAsync(string path, Stream data, string contentType, CancellationToken ct = default)
public async Task PutObjectAsync(
string path,
Stream data,
string contentType,
CancellationToken ct = default
)
{
_logger.Debug("Putting object at path {Path} with length {Length} and content type {ContentType}", path,
data.Length, contentType);
_logger.Debug(
"Putting object at path {Path} with length {Length} and content type {ContentType}",
path,
data.Length,
contentType
);
await minioClient.PutObjectAsync(new PutObjectArgs()
await minioClient.PutObjectAsync(
new PutObjectArgs()
.WithBucket(config.Storage.Bucket)
.WithObject(path)
.WithObjectSize(data.Length)
.WithStreamData(data)
.WithContentType(contentType), ct
.WithContentType(contentType),
ct
);
}
}
}

View file

@ -23,4 +23,4 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B
var keyCacheSvc = scope.ServiceProvider.GetRequiredService<KeyCacheService>();
await keyCacheSvc.DeleteExpiredKeysAsync(ct);
}
}
}

View file

@ -11,30 +11,42 @@ public class RemoteAuthService(Config config, ILogger logger)
private readonly Uri _discordTokenUri = new("https://discord.com/api/oauth2/token");
private readonly Uri _discordUserUri = new("https://discord.com/api/v10/users/@me");
public async Task<RemoteUser> RequestDiscordTokenAsync(string code, string state, CancellationToken ct = default)
public async Task<RemoteUser> RequestDiscordTokenAsync(
string code,
string state,
CancellationToken ct = default
)
{
var redirectUri = $"{config.BaseUrl}/auth/callback/discord";
var resp = await _httpClient.PostAsync(_discordTokenUri, new FormUrlEncodedContent(
new Dictionary<string, string>
{
{ "client_id", config.DiscordAuth.ClientId! },
{ "client_secret", config.DiscordAuth.ClientSecret! },
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", redirectUri }
}
), ct);
var resp = await _httpClient.PostAsync(
_discordTokenUri,
new FormUrlEncodedContent(
new Dictionary<string, string>
{
{ "client_id", config.DiscordAuth.ClientId! },
{ "client_secret", config.DiscordAuth.ClientSecret! },
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", redirectUri },
}
),
ct
);
if (!resp.IsSuccessStatusCode)
{
var respBody = await resp.Content.ReadAsStringAsync(ct);
_logger.Error("Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}",
(int)resp.StatusCode, respBody);
_logger.Error(
"Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}",
(int)resp.StatusCode,
respBody
);
throw new FoxnounsError("Invalid Discord OAuth response");
}
resp.EnsureSuccessStatusCode();
var token = await resp.Content.ReadFromJsonAsync<DiscordTokenResponse>(ct);
if (token == null) throw new FoxnounsError("Discord token response was null");
if (token == null)
throw new FoxnounsError("Discord token response was null");
var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri);
req.Headers.Add("Authorization", $"{token.token_type} {token.access_token}");
@ -42,20 +54,27 @@ public class RemoteAuthService(Config config, ILogger logger)
var resp2 = await _httpClient.SendAsync(req, ct);
resp2.EnsureSuccessStatusCode();
var user = await resp2.Content.ReadFromJsonAsync<DiscordUserResponse>(ct);
if (user == null) throw new FoxnounsError("Discord user response was null");
if (user == null)
throw new FoxnounsError("Discord user response was null");
return new RemoteUser(user.id, user.username);
}
[SuppressMessage("ReSharper", "InconsistentNaming",
Justification = "Easier to use snake_case here, rather than passing in JSON converter options")]
[SuppressMessage(
"ReSharper",
"InconsistentNaming",
Justification = "Easier to use snake_case here, rather than passing in JSON converter options"
)]
[UsedImplicitly]
private record DiscordTokenResponse(string access_token, string token_type);
[SuppressMessage("ReSharper", "InconsistentNaming",
Justification = "Easier to use snake_case here, rather than passing in JSON converter options")]
[SuppressMessage(
"ReSharper",
"InconsistentNaming",
Justification = "Easier to use snake_case here, rather than passing in JSON converter options"
)]
[UsedImplicitly]
private record DiscordUserResponse(string id, string username);
public record RemoteUser(string Id, string Username);
}
}

View file

@ -7,48 +7,73 @@ using NodaTime;
namespace Foxnouns.Backend.Services;
public class UserRendererService(DatabaseContext db, MemberRendererService memberRenderer, Config config)
public class UserRendererService(
DatabaseContext db,
MemberRendererService memberRenderer,
Config config
)
{
public async Task<UserResponse> RenderUserAsync(User user, User? selfUser = null,
public async Task<UserResponse> RenderUserAsync(
User user,
User? selfUser = null,
Token? token = null,
bool renderMembers = true,
bool renderAuthMethods = false,
CancellationToken ct = default)
CancellationToken ct = default
)
{
var isSelfUser = selfUser?.Id == user.Id;
var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser;
var tokenHidden = token.HasScope("user.read_hidden") && isSelfUser;
var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser;
renderMembers = renderMembers &&
(!user.ListHidden || tokenCanReadHiddenMembers);
renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers);
renderAuthMethods = renderAuthMethods && tokenPrivileged;
IEnumerable<Member> members =
renderMembers ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) : [];
IEnumerable<Member> members = renderMembers
? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct)
: [];
// Unless the user is requesting their own members AND the token can read hidden members, we filter out unlisted members.
if (!(isSelfUser && tokenCanReadHiddenMembers)) members = members.Where(m => !m.Unlisted);
if (!(isSelfUser && tokenCanReadHiddenMembers))
members = members.Where(m => !m.Unlisted);
var flags = await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct);
var flags = await db
.UserFlags.Where(f => f.UserId == user.Id)
.OrderBy(f => f.Id)
.ToListAsync(ct);
var authMethods = renderAuthMethods
? await db.AuthMethods
.Where(a => a.UserId == user.Id)
? await db
.AuthMethods.Where(a => a.UserId == user.Id)
.Include(a => a.FediverseApplication)
.ToListAsync(ct)
: [];
return new UserResponse(
user.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user),
user.Id,
user.Sid,
user.Username,
user.DisplayName,
user.Bio,
user.MemberTitle,
AvatarUrlFor(user),
user.Links,
user.Names, user.Pronouns, user.Fields, user.CustomPreferences,
user.Names,
user.Pronouns,
user.Fields,
user.CustomPreferences,
flags.Select(f => RenderPrideFlag(f.PrideFlag)),
user.Role,
renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null,
renderMembers
? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden))
: null,
renderAuthMethods
? authMethods.Select(a => new AuthenticationMethodResponse(
a.Id, a.AuthType, a.RemoteId,
a.RemoteUsername, a.FediverseApplication?.Domain
a.Id,
a.AuthType,
a.RemoteId,
a.RemoteUsername,
a.FediverseApplication?.Domain
))
: null,
tokenHidden ? user.ListHidden : null,
@ -58,10 +83,19 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
}
public PartialUser RenderPartialUser(User user) =>
new(user.Id, user.Sid, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences);
new(
user.Id,
user.Sid,
user.Username,
user.DisplayName,
AvatarUrlFor(user),
user.CustomPreferences
);
private string? AvatarUrlFor(User user) =>
user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null;
user.Avatar != null
? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp"
: null;
public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp";
@ -79,29 +113,26 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
IEnumerable<Field> Fields,
Dictionary<Snowflake, User.CustomPreference> CustomPreferences,
IEnumerable<PrideFlagResponse> Flags,
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
UserRole Role,
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
IEnumerable<MemberRendererService.PartialMember>? Members,
IEnumerable<MemberRendererService.PartialMember>? Members,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
IEnumerable<AuthenticationMethodResponse>? AuthMethods,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
bool? MemberListHidden,
bool? MemberListHidden,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
Instant? LastActive,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
Instant? LastSidReroll
Instant? LastSidReroll
);
public record AuthenticationMethodResponse(
Snowflake Id,
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
AuthType Type,
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
string RemoteId,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
string? RemoteUsername,
string? RemoteUsername,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
string? FediverseInstance
string? FediverseInstance
);
public record PartialUser(
@ -120,5 +151,6 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
Snowflake Id,
string ImageUrl,
string Name,
string? Description);
}
string? Description
);
}