From 30bf18a53911cf555df6e965556608d4709f0f22 Mon Sep 17 00:00:00 2001 From: notfire Date: Fri, 11 Apr 2025 18:38:55 -0400 Subject: [PATCH] e --- .../inspectionProfiles/Project_Default.xml | 5 + .../Controllers/Mastodon/AuthController.cs | 42 +++++++ .../Mastodon/Renderers/UserRenderer.cs | 49 +++++++- .../Schemas/Entities/AccountEntity.cs | 51 +++++---- .../Controllers/Pleroma/AdminController.cs | 106 ++++++++++++++++++ .../Schemas/Entities/AkkomaInstanceEntity.cs | 9 ++ .../Schemas/Entities/AkkomaNodeInfoEntity.cs | 8 ++ .../Entities/AkkomaNodeInfoSoftwareEntity.cs | 9 ++ .../Schemas/Entities/AkkomaUserExtensions.cs | 10 ++ .../Entities/PleromaOauthTokenEntity.cs | 11 ++ .../Schemas/Entities/PleromaUserExtensions.cs | 12 ++ .../Pleroma/Schemas/ReportsResponse.cs | 22 ++++ .../Properties/launchSettings.json | 1 + Iceshrimp.Backend/configuration.ini | 6 +- 14 files changed, 312 insertions(+), 29 deletions(-) create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/AdminController.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaInstanceEntity.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoEntity.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoSoftwareEntity.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaUserExtensions.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaOauthTokenEntity.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaUserExtensions.cs create mode 100644 Iceshrimp.Backend/Controllers/Pleroma/Schemas/ReportsResponse.cs diff --git a/.idea/.idea.Iceshrimp.NET/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.Iceshrimp.NET/.idea/inspectionProfiles/Project_Default.xml index 9f91588a..e9d94835 100644 --- a/.idea/.idea.Iceshrimp.NET/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/.idea.Iceshrimp.NET/.idea/inspectionProfiles/Project_Default.xml @@ -2,6 +2,11 @@ \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs b/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs index 29900b75..9cda530a 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Mime; using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; using Iceshrimp.Backend.Controllers.Shared.Attributes; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; @@ -8,6 +9,7 @@ using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; @@ -157,4 +159,44 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa return new object(); } + + [Authenticate] + [HttpGet("/api/oauth_tokens.json")] + [ProducesResults(HttpStatusCode.OK)] + public async Task> GetOauthTokens() + { + var user = HttpContext.GetUserOrFail(); + var oauthTokens = await db.OauthTokens + .Where(p => p.User == user) + .Include(oauthToken => oauthToken.App) + .ToListAsync(); + + List result = []; + foreach (var token in oauthTokens) + { + result.Add(new PleromaOauthTokenEntity() + { + Id = token.Id, + AppName = token.App.Name, + ValidUntil = token.CreatedAt + TimeSpan.FromDays(365 * 100) + }); + } + + return result; + } + + [Authenticate] + [HttpDelete("/api/oauth_tokens/{id}")] + [ProducesResults(HttpStatusCode.Created)] + [ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)] + public async Task RevokeOauthTokenPleroma(string id) + { + var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Id == id) ?? + throw GracefulException.Forbidden("You are not authorized to revoke this token"); + + db.Remove(token); + await db.SaveChangesAsync(); + + Response.StatusCode = 201; + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs index b793465d..ca0b163d 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/UserRenderer.cs @@ -1,9 +1,11 @@ using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; +using Iceshrimp.Backend.Core.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -13,7 +15,8 @@ public class UserRenderer( IOptions config, IOptionsSnapshot security, MfmConverter mfmConverter, - DatabaseContext db + DatabaseContext db, + FlagService flags ) : IScopedService { private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png"; @@ -48,6 +51,25 @@ public class UserRenderer( var avatarAlt = data?.AvatarAlt.GetValueOrDefault(user.Id); var bannerAlt = data?.BannerAlt.GetValueOrDefault(user.Id); + string? favicon; + string? softwareName; + string? softwareVersion; + if (user.IsRemoteUser) + { + var instInfo = await db.Instances + .Where(p => p.Host == user.Host) + .FirstOrDefaultAsync(); + favicon = instInfo?.FaviconUrl; + softwareName = instInfo?.SoftwareName; + softwareVersion = instInfo?.SoftwareVersion; + } + else + { + favicon = config.Value.WebDomain + "/_content/Iceshrimp.Assets.Branding/favicon.png"; + softwareName = "iceshrimp"; + softwareVersion = config.Value.Version; + } + var res = new AccountEntity { Id = user.Id, @@ -74,7 +96,30 @@ public class UserRenderer( IsBot = user.IsBot, IsDiscoverable = user.IsExplorable, Fields = fields?.ToList() ?? [], - Emoji = profileEmoji + Emoji = profileEmoji, + Pleroma = flags?.IsPleroma.Value == true + ? new PleromaUserExtensions + { + IsAdmin = user.IsAdmin, + IsModerator = user.IsModerator, + Favicon = favicon! + } : null, + Akkoma = flags?.IsPleroma.Value == true + ? new AkkomaUserExtensions + { + Instance = new AkkomaInstanceEntity + { + Name = user.Host ?? config.Value.AccountDomain, + NodeInfo = new AkkomaNodeInfoEntity + { + Software = new AkkomaNodeInfoSoftwareEntity + { + Name = softwareName, + Version = softwareVersion + } + } + } + } : null }; if (localUser is null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AccountEntity.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AccountEntity.cs index 576fa109..84daa6f6 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AccountEntity.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/AccountEntity.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; using Iceshrimp.Shared.Helpers; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; @@ -5,30 +6,32 @@ namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; public class AccountEntity : IIdentifiable { - [J("username")] public required string Username { get; set; } - [J("acct")] public required string Acct { get; set; } - [J("fqn")] public required string FullyQualifiedName { get; set; } - [J("display_name")] public required string DisplayName { get; set; } - [J("locked")] public required bool IsLocked { get; set; } - [J("created_at")] public required string CreatedAt { get; set; } - [J("followers_count")] public required long FollowersCount { get; set; } - [J("following_count")] public required long FollowingCount { get; set; } - [J("statuses_count")] public required long StatusesCount { get; set; } - [J("note")] public required string Note { get; set; } - [J("url")] public required string Url { get; set; } - [J("uri")] public required string Uri { get; set; } - [J("avatar")] public required string AvatarUrl { get; set; } - [J("avatar_static")] public required string AvatarStaticUrl { get; set; } - [J("header")] public required string HeaderUrl { get; set; } - [J("header_static")] public required string HeaderStaticUrl { get; set; } - [J("moved")] public required AccountEntity? MovedToAccount { get; set; } - [J("bot")] public required bool IsBot { get; set; } - [J("discoverable")] public required bool IsDiscoverable { get; set; } - [J("fields")] public required List Fields { get; set; } - [J("source")] public AccountSource? Source { get; set; } - [J("emojis")] public required List Emoji { get; set; } - [J("id")] public required string Id { get; set; } - [J("last_status_at")] public string? LastStatusAt { get; set; } + [J("username")] public required string Username { get; set; } + [J("acct")] public required string Acct { get; set; } + [J("fqn")] public required string FullyQualifiedName { get; set; } + [J("display_name")] public required string DisplayName { get; set; } + [J("locked")] public required bool IsLocked { get; set; } + [J("created_at")] public required string CreatedAt { get; set; } + [J("followers_count")] public required long FollowersCount { get; set; } + [J("following_count")] public required long FollowingCount { get; set; } + [J("statuses_count")] public required long StatusesCount { get; set; } + [J("note")] public required string Note { get; set; } + [J("url")] public required string Url { get; set; } + [J("uri")] public required string Uri { get; set; } + [J("avatar")] public required string AvatarUrl { get; set; } + [J("avatar_static")] public required string AvatarStaticUrl { get; set; } + [J("header")] public required string HeaderUrl { get; set; } + [J("header_static")] public required string HeaderStaticUrl { get; set; } + [J("moved")] public required AccountEntity? MovedToAccount { get; set; } + [J("bot")] public required bool IsBot { get; set; } + [J("discoverable")] public required bool IsDiscoverable { get; set; } + [J("fields")] public required List Fields { get; set; } + [J("source")] public AccountSource? Source { get; set; } + [J("emojis")] public required List Emoji { get; set; } + [J("id")] public required string Id { get; set; } + [J("last_status_at")] public string? LastStatusAt { get; set; } + [J("pleroma")] public required PleromaUserExtensions? Pleroma { get; set; } + [J("akkoma")] public required AkkomaUserExtensions? Akkoma { get; set; } [J("avatar_description")] public required string AvatarDescription { get; set; } [J("header_description")] public required string HeaderDescription { get; set; } diff --git a/Iceshrimp.Backend/Controllers/Pleroma/AdminController.cs b/Iceshrimp.Backend/Controllers/Pleroma/AdminController.cs new file mode 100644 index 00000000..5374bc71 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/AdminController.cs @@ -0,0 +1,106 @@ +using System.Net; +using System.Net.Mime; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Pleroma.Schemas; +using Iceshrimp.Backend.Controllers.Shared.Attributes; +using Iceshrimp.Backend.Controllers.Web.Renderers; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Extensions; +using Iceshrimp.Backend.Core.Middleware; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.EntityFrameworkCore; +using NoteRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.NoteRenderer; +using UserRenderer = Iceshrimp.Backend.Controllers.Mastodon.Renderers.UserRenderer; + +namespace Iceshrimp.Backend.Controllers.Pleroma; + +[MastodonApiController] +[Authenticate] +[EnableCors("mastodon")] +[EnableRateLimiting("sliding")] +[Produces(MediaTypeNames.Application.Json)] +public class AdminController( + DatabaseContext db, + ReportRenderer reportRenderer, + NoteRenderer noteRenderer, + UserRenderer userRenderer +) : ControllerBase +{ + [HttpGet("/api/v1/pleroma/admin/reports")] + [Authenticate("admin:read:reports")] + [ProducesResults(HttpStatusCode.OK)] + public async Task GetReports() + { + var user = HttpContext.GetUserOrFail(); + var reports = await db.Reports + .IncludeCommonProperties() + .ToListAsync(); + + var rendered = await reportRenderer.RenderManyAsync(reports); + + var reportsList = new List(); + foreach (var r in rendered) + { + var reActor = await db.Users + .IncludeCommonProperties() + .Where(p => p.Id == r.Reporter.Id) + .RenderAllForMastodonAsync(userRenderer, user); + + var reTarget = await db.Users + .IncludeCommonProperties() + .Where(p => p.Id == r.TargetUser.Id) + .RenderAllForMastodonAsync(userRenderer, user); + + foreach (var n in r.Notes) + { + var note = await db.Notes + .IncludeCommonProperties() + .Where(p => p.Id == n.Id) + .RenderAllForMastodonAsync(noteRenderer, user); + + reportsList.Add(new Reports() + { + Account = reTarget.FirstOrDefault()!, + Actor = reActor.FirstOrDefault()!, + Id = r.Id, + CreatedAt = r.CreatedAt, + State = r.Resolved ? "resolved" : "open", + Content = r.Comment, + Statuses = note, + Notes = [] // unsupported + }); + } + + } + + var resps = new ReportsQuery() + { + Total = reportsList.Count, + Reports = reportsList + }; + + return resps; + } + + [HttpPatch("/api/v1/pleroma/admin/reports")] + [Authenticate("admin:read:reports")] + [ProducesResults(HttpStatusCode.OK)] + [ProducesErrors(HttpStatusCode.NotFound)] + // ReSharper disable once AsyncVoidMethod + public async Task? SetReportState(ReportsQuery query) + { + foreach (var list in query.Reports) + { + var report = await db.Reports.Where(p => p.Id == list.Id).FirstOrDefaultAsync() + ?? throw GracefulException.NotFound("Report not found"); + + report.Resolved = list.State is "resolved" or "closed"; + + await db.SaveChangesAsync(); + } + + return query; + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaInstanceEntity.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaInstanceEntity.cs new file mode 100644 index 00000000..97b6d254 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaInstanceEntity.cs @@ -0,0 +1,9 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +public class AkkomaInstanceEntity +{ + [J("name")] public required string Name { get; set; } + [J("nodeinfo")] public required AkkomaNodeInfoEntity NodeInfo { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoEntity.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoEntity.cs new file mode 100644 index 00000000..d712dab5 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoEntity.cs @@ -0,0 +1,8 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +public class AkkomaNodeInfoEntity +{ + [J("software")] public required AkkomaNodeInfoSoftwareEntity Software { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoSoftwareEntity.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoSoftwareEntity.cs new file mode 100644 index 00000000..ccccbe5d --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaNodeInfoSoftwareEntity.cs @@ -0,0 +1,9 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +public class AkkomaNodeInfoSoftwareEntity +{ + [J("name")] public required string? Name { get; set; } + [J("version")] public required string? Version { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaUserExtensions.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaUserExtensions.cs new file mode 100644 index 00000000..d2cd6891 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/AkkomaUserExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +[Keyless] +public class AkkomaUserExtensions +{ + [J("instance")] public required AkkomaInstanceEntity Instance { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaOauthTokenEntity.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaOauthTokenEntity.cs new file mode 100644 index 00000000..59861b07 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaOauthTokenEntity.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices.JavaScript; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +public class PleromaOauthTokenEntity +{ + [J("id")] public required string Id { get; set; } + [J("valid_until")] public required DateTime ValidUntil { get; set; } + [J("app_name")] public required string? AppName { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaUserExtensions.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaUserExtensions.cs new file mode 100644 index 00000000..3f817d51 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/Entities/PleromaUserExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas.Entities; + +[Keyless] +public class PleromaUserExtensions +{ + [J("is_admin")] public required bool IsAdmin { get; set; } + [J("is_moderator")] public required bool IsModerator { get; set; } + [J("favicon")] public required string Favicon { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Pleroma/Schemas/ReportsResponse.cs b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/ReportsResponse.cs new file mode 100644 index 00000000..3ac9538c --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Pleroma/Schemas/ReportsResponse.cs @@ -0,0 +1,22 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Pleroma.Schemas; + +public class ReportsQuery +{ + [J("total")] public int Total { get; set; } + [J("reports")] public required List Reports { get; set; } +} + +public class Reports +{ + [J("account")] public AccountEntity? Account { get; set; } + [J("actor")] public AccountEntity? Actor { get; set; } + [J("id")] public required string Id { get; set; } + [J("created_at")] public DateTime? CreatedAt { get; set; } + [J("state")] public required string State { get; set; } + [J("content")] public string? Content { get; set; } + [J("statuses")] public IEnumerable? Statuses { get; set; } + [J("notes")] public string[]? Notes { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Properties/launchSettings.json b/Iceshrimp.Backend/Properties/launchSettings.json index 8406b1ed..4a336233 100644 --- a/Iceshrimp.Backend/Properties/launchSettings.json +++ b/Iceshrimp.Backend/Properties/launchSettings.json @@ -6,6 +6,7 @@ "dotnetRunMessages": true, "launchBrowser": false, "externalUrlConfiguration": true, + "commandLineArgs": "--migrate-and-start", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Iceshrimp.Backend/configuration.ini b/Iceshrimp.Backend/configuration.ini index b72a8e8e..b5b2c20a 100644 --- a/Iceshrimp.Backend/configuration.ini +++ b/Iceshrimp.Backend/configuration.ini @@ -7,8 +7,8 @@ ListenHost = localhost ;;ListenSocketPerms = 660 ;; Caution: changing these settings after initial setup *will* break federation -WebDomain = shrimp.example.org -AccountDomain = example.org +WebDomain = localhost:3000 +AccountDomain = localhost:3000 ;; End of problematic settings block ;; Additional domains this instance allows API access from, separated by commas. @@ -184,7 +184,7 @@ ProxyRemoteMedia = true [Storage:Local] ;; Path where media is stored at. Must be writable for the service user. -Path = /path/to/media/location +Path = /home/luke/Documents/shrimp [Storage:ObjectStorage] ;;Endpoint = endpoint.example.org