Compare commits

..

No commits in common. "478ba2a4065f1aa18e2dc5c5f8f9db761ece6e16" and "7791c9196091d9c6bbeb2fda46f0913b925cc4b7" have entirely different histories.

15 changed files with 38 additions and 458 deletions

View file

@ -1,17 +1,3 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Middleware;
using Microsoft.AspNetCore.Mvc;

View file

@ -38,8 +38,6 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
{
if (template.StartsWith("api/v2"))
template = template["api/v2".Length..];
else if (template.StartsWith("api/v1"))
template = template["api/v1".Length..];
template = PathVarRegex()
.Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}`
.Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}`

View file

@ -1,17 +1,3 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Middleware;

View file

@ -0,0 +1,16 @@
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Services.V1;
using Microsoft.AspNetCore.Mvc;
namespace Foxnouns.Backend.Controllers.V1;
[Route("/api/v1/users")]
public class UsersV1Controller(UsersV1Service usersV1Service) : ApiControllerBase
{
[HttpGet("{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));
}
}

View file

@ -1,111 +0,0 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto.V1;
using Foxnouns.Backend.Services.V1;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Foxnouns.Backend.Controllers.V1;
[Route("/api/v1")]
public class V1ReadController(
UsersV1Service usersV1Service,
MembersV1Service membersV1Service,
DatabaseContext db
) : ApiControllerBase
{
[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,
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
)
);
}
[HttpGet("users/{userRef}/members")]
public async Task<IActionResult> GetUserMembersAsync(
string userRef,
CancellationToken ct = default
)
{
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
List<Member> members = await db
.Members.Where(m => m.UserId == user.Id)
.OrderBy(m => m.Name)
.ToListAsync(ct);
List<MemberResponse> responses = [];
foreach (Member member in members)
{
responses.Add(
await membersV1Service.RenderMemberAsync(
member,
CurrentToken,
user,
renderFlags: true,
ct: ct
)
);
}
return Ok(responses);
}
[HttpGet("users/{userRef}/members/{memberRef}")]
public async Task<IActionResult> GetUserMemberAsync(
string userRef,
string memberRef,
CancellationToken ct = default
)
{
Member member = await membersV1Service.ResolveMemberAsync(
userRef,
memberRef,
CurrentToken,
ct
);
return Ok(
await membersV1Service.RenderMemberAsync(
member,
CurrentToken,
renderFlags: true,
ct: ct
)
);
}
}

View file

@ -1,59 +0,0 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// 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

@ -1,18 +1,3 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// ReSharper disable NotAccessedPositionalProperty.Global
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
@ -36,8 +21,6 @@ public record UserResponse(
FieldEntry[] Names,
PronounEntry[] Pronouns,
ProfileField[] Fields,
PrideFlag[] Flags,
PartialMember[] Members,
int? UtcOffset,
Dictionary<Guid, CustomPreference> CustomPreferences
);
@ -92,5 +75,3 @@ 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,8 +130,7 @@ public static class WebApplicationExtensions
.AddTransient<CreateFlagInvocable>()
.AddTransient<CreateDataExportInvocable>()
// Legacy services
.AddScoped<UsersV1Service>()
.AddScoped<MembersV1Service>();
.AddScoped<UsersV1Service>();
if (!config.Logging.EnableMetrics)
services.AddHostedService<BackgroundMetricsCollectionService>();

View file

@ -1,125 +0,0 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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, UsersV1Service usersV1Service)
{
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<Member> ResolveMemberAsync(
string userRef,
string memberRef,
Token? token,
CancellationToken ct = default
)
{
User user = await usersV1Service.ResolveUserAsync(userRef, token, ct);
Member? member;
if (Snowflake.TryParse(memberRef, out Snowflake? sf))
{
member = await db
.Members.Include(m => m.User)
.FirstOrDefaultAsync(m => m.Id == sf && m.UserId == user.Id, ct);
if (member != null)
return member;
}
member = await db
.Members.Include(m => m.User)
.FirstOrDefaultAsync(m => m.LegacyId == memberRef && m.UserId == user.Id, ct);
if (member != null)
return member;
member = await db
.Members.Include(m => m.User)
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == user.Id, ct);
if (member != null)
return member;
throw new ApiError.NotFound(
"No member with that ID or name found.",
ErrorCode.MemberNotFound
);
}
public async Task<MemberResponse> RenderMemberAsync(
Member m,
Token? token = default,
User? user = null,
bool renderFlags = true,
CancellationToken ct = default
)
{
user ??= m.User;
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, user.CustomPreferences),
Pronouns: PronounEntry.FromPronouns(m.Pronouns, user.CustomPreferences),
Fields: ProfileField.FromFields(m.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(),
User: UsersV1Service.RenderPartialUser(user),
Unlisted: renderUnlisted ? m.Unlisted : null
);
}
}

View file

@ -1,23 +1,8 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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;
@ -64,26 +49,8 @@ public class UsersV1Service(DatabaseContext db)
);
}
public async Task<UserResponse> RenderUserAsync(
User user,
Token? token = null,
bool renderMembers = true,
bool renderFlags = true,
CancellationToken ct = default
)
public async Task<UserResponse> RenderUserAsync(User user)
{
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
@ -103,67 +70,23 @@ public class UsersV1Service(DatabaseContext db)
user.MemberTitle,
user.Avatar,
user.Links,
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(),
FieldEntry.FromEntries(user.Names, user.CustomPreferences),
PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences),
ProfileField.FromFields(user.Fields, user.CustomPreferences),
utcOffset,
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
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();
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)
.ToDictionary()
);
}
}

View file

@ -1,17 +1,3 @@
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;

View file

@ -1,7 +1,7 @@
; The host the server will listen on
Host = localhost
; The port the server will listen on
Port = 6000
Port = 5000
; The base *external* URL
BaseUrl = https://pronouns.localhost
; The base URL for media, without a trailing slash. This must be publicly accessible.

View file

@ -1,7 +1,7 @@
# Example .env file--DO NOT EDIT, copy to .env or .env.local then edit
# Example .env file--DO NOT EDIT
PUBLIC_LANGUAGE=en
PUBLIC_BASE_URL=https://pronouns.cc
PUBLIC_SHORT_URL=https://prns.cc
PUBLIC_API_BASE=https://pronouns.cc/api
PRIVATE_API_HOST=http://localhost:5003/api
PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api
PRIVATE_INTERNAL_API_HOST=http://localhost:5000/api

View file

@ -38,7 +38,7 @@ func (hn *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// all public api endpoints are prefixed with this
if !strings.HasPrefix(r.URL.Path, "/api/v2") && !strings.HasPrefix(r.URL.Path, "/api/v1") {
if !strings.HasPrefix(r.URL.Path, "/api/v2") {
w.WriteHeader(http.StatusNotFound)
return
}

View file

@ -1,6 +1,6 @@
{
"port": 5003,
"proxy_target": "http://localhost:6000",
"proxy_target": "http://localhost:5000",
"debug": true,
"powered_by": "5 gay rats"
}