diff --git a/Foxnouns.Backend/Controllers/DeleteUserController.cs b/Foxnouns.Backend/Controllers/DeleteUserController.cs index b611c35..d1c8e62 100644 --- a/Foxnouns.Backend/Controllers/DeleteUserController.cs +++ b/Foxnouns.Backend/Controllers/DeleteUserController.cs @@ -1,3 +1,17 @@ +// 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 . using Foxnouns.Backend.Database; using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc; diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 85bc774..3954547 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -38,6 +38,8 @@ 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}` diff --git a/Foxnouns.Backend/Controllers/NotificationsController.cs b/Foxnouns.Backend/Controllers/NotificationsController.cs index f258b3c..873344c 100644 --- a/Foxnouns.Backend/Controllers/NotificationsController.cs +++ b/Foxnouns.Backend/Controllers/NotificationsController.cs @@ -1,3 +1,17 @@ +// 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 . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs b/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs deleted file mode 100644 index e11e490..0000000 --- a/Foxnouns.Backend/Controllers/V1/UsersV1Controller.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 GetUserAsync(string userRef, CancellationToken ct = default) - { - User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok(await usersV1Service.RenderUserAsync(user)); - } -} diff --git a/Foxnouns.Backend/Controllers/V1/V1ReadController.cs b/Foxnouns.Backend/Controllers/V1/V1ReadController.cs new file mode 100644 index 0000000..5f69c20 --- /dev/null +++ b/Foxnouns.Backend/Controllers/V1/V1ReadController.cs @@ -0,0 +1,111 @@ +// 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 . +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 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 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 GetUserMembersAsync( + string userRef, + CancellationToken ct = default + ) + { + User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); + List members = await db + .Members.Where(m => m.UserId == user.Id) + .OrderBy(m => m.Name) + .ToListAsync(ct); + + List 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 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 + ) + ); + } +} diff --git a/Foxnouns.Backend/Dto/V1/Member.cs b/Foxnouns.Backend/Dto/V1/Member.cs new file mode 100644 index 0000000..c745187 --- /dev/null +++ b/Foxnouns.Backend/Dto/V1/Member.cs @@ -0,0 +1,59 @@ +// 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 . + +// 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 CustomPreferences +); diff --git a/Foxnouns.Backend/Dto/V1/User.cs b/Foxnouns.Backend/Dto/V1/User.cs index eab4c29..c212d97 100644 --- a/Foxnouns.Backend/Dto/V1/User.cs +++ b/Foxnouns.Backend/Dto/V1/User.cs @@ -1,3 +1,18 @@ +// 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 . + // ReSharper disable NotAccessedPositionalProperty.Global using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -21,6 +36,8 @@ public record UserResponse( FieldEntry[] Names, PronounEntry[] Pronouns, ProfileField[] Fields, + PrideFlag[] Flags, + PartialMember[] Members, int? UtcOffset, Dictionary CustomPreferences ); @@ -75,3 +92,5 @@ public record PronounEntry(string Pronouns, string? DisplayText, string Status) )) .ToArray(); } + +public record PrideFlag(string Id, Snowflake IdNew, string Hash, string Name, string? Description); diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 86b4a82..426ec12 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -130,7 +130,8 @@ public static class WebApplicationExtensions .AddTransient() .AddTransient() // Legacy services - .AddScoped(); + .AddScoped() + .AddScoped(); if (!config.Logging.EnableMetrics) services.AddHostedService(); diff --git a/Foxnouns.Backend/Services/V1/MembersV1Service.cs b/Foxnouns.Backend/Services/V1/MembersV1Service.cs new file mode 100644 index 0000000..632226c --- /dev/null +++ b/Foxnouns.Backend/Services/V1/MembersV1Service.cs @@ -0,0 +1,125 @@ +// 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 . +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 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 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 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 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 + ); + } +} diff --git a/Foxnouns.Backend/Services/V1/UsersV1Service.cs b/Foxnouns.Backend/Services/V1/UsersV1Service.cs index 7492256..34163a6 100644 --- a/Foxnouns.Backend/Services/V1/UsersV1Service.cs +++ b/Foxnouns.Backend/Services/V1/UsersV1Service.cs @@ -1,8 +1,23 @@ +// 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 . 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; @@ -49,8 +64,26 @@ public class UsersV1Service(DatabaseContext db) ); } - public async Task RenderUserAsync(User user) + public async Task 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 members = renderMembers + ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) + : []; + + List 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 +103,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 RenderCustomPreferences( + Dictionary 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 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) + ); } diff --git a/Foxnouns.Backend/Services/V1/V1Utils.cs b/Foxnouns.Backend/Services/V1/V1Utils.cs index eb8d9c0..2e52316 100644 --- a/Foxnouns.Backend/Services/V1/V1Utils.cs +++ b/Foxnouns.Backend/Services/V1/V1Utils.cs @@ -1,3 +1,17 @@ +// 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 . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index 7522cba..9c6097e 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -1,7 +1,7 @@ ; The host the server will listen on Host = localhost ; The port the server will listen on -Port = 5000 +Port = 6000 ; The base *external* URL BaseUrl = https://pronouns.localhost ; The base URL for media, without a trailing slash. This must be publicly accessible. diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example index d79c672..2931832 100644 --- a/Foxnouns.Frontend/.env.example +++ b/Foxnouns.Frontend/.env.example @@ -1,7 +1,7 @@ -# Example .env file--DO NOT EDIT +# Example .env file--DO NOT EDIT, copy to .env or .env.local then 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:5000/api +PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api diff --git a/rate/handler.go b/rate/handler.go index 7ab0b59..311b5b8 100644 --- a/rate/handler.go +++ b/rate/handler.go @@ -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") { + if !strings.HasPrefix(r.URL.Path, "/api/v2") && !strings.HasPrefix(r.URL.Path, "/api/v1") { w.WriteHeader(http.StatusNotFound) return } diff --git a/rate/proxy-config.example.json b/rate/proxy-config.example.json index 427acef..1ec9e59 100644 --- a/rate/proxy-config.example.json +++ b/rate/proxy-config.example.json @@ -1,6 +1,6 @@ { "port": 5003, - "proxy_target": "http://localhost:5000", + "proxy_target": "http://localhost:6000", "debug": true, "powered_by": "5 gay rats" }