[backend/masto-client] Add schemas and renderers for user statuses

This commit is contained in:
Laura Hausmann 2024-02-02 02:35:03 +01:00
parent 09f9d2395c
commit aa6988399d
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
8 changed files with 139 additions and 22 deletions

View file

@ -18,13 +18,13 @@ public class MastodonAuthController(DatabaseContext db) : Controller {
[HttpGet("/api/v1/apps/verify_credentials")] [HttpGet("/api/v1/apps/verify_credentials")]
[AuthenticateOauth] [AuthenticateOauth]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Auth.VerifyAppCredentialsResponse))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.VerifyAppCredentialsResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
public IActionResult VerifyAppCredentials() { public IActionResult VerifyAppCredentials() {
var token = HttpContext.GetOauthToken(); var token = HttpContext.GetOauthToken();
if (token == null) throw GracefulException.Unauthorized("The access token is invalid"); if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
var res = new Auth.VerifyAppCredentialsResponse { var res = new AuthSchemas.VerifyAppCredentialsResponse {
App = token.App, App = token.App,
VapidKey = null //FIXME VapidKey = null //FIXME
}; };
@ -36,9 +36,9 @@ public class MastodonAuthController(DatabaseContext db) : Controller {
[EnableRateLimiting("strict")] [EnableRateLimiting("strict")]
[ConsumesHybrid] [ConsumesHybrid]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Auth.RegisterAppResponse))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.RegisterAppResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
public async Task<IActionResult> RegisterApp([FromHybrid] Auth.RegisterAppRequest request) { public async Task<IActionResult> RegisterApp([FromHybrid] AuthSchemas.RegisterAppRequest request) {
if (request.RedirectUris.Count == 0) if (request.RedirectUris.Count == 0)
throw GracefulException.BadRequest("Invalid redirect_uris parameter"); throw GracefulException.BadRequest("Invalid redirect_uris parameter");
@ -71,7 +71,7 @@ public class MastodonAuthController(DatabaseContext db) : Controller {
await db.AddAsync(app); await db.AddAsync(app);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
var res = new Auth.RegisterAppResponse { var res = new AuthSchemas.RegisterAppResponse {
App = app, App = app,
VapidKey = null //FIXME VapidKey = null //FIXME
}; };
@ -83,9 +83,9 @@ public class MastodonAuthController(DatabaseContext db) : Controller {
[HttpPost("/oauth/token")] [HttpPost("/oauth/token")]
[ConsumesHybrid] [ConsumesHybrid]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Auth.OauthTokenResponse))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.OauthTokenResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
public async Task<IActionResult> GetOauthToken([FromHybrid] Auth.OauthTokenRequest request) { public async Task<IActionResult> GetOauthToken([FromHybrid] AuthSchemas.OauthTokenRequest request) {
//TODO: app-level access (grant_type = "client_credentials") //TODO: app-level access (grant_type = "client_credentials")
if (request.GrantType != "authorization_code") if (request.GrantType != "authorization_code")
throw GracefulException.BadRequest("Invalid grant_type"); throw GracefulException.BadRequest("Invalid grant_type");
@ -108,7 +108,7 @@ public class MastodonAuthController(DatabaseContext db) : Controller {
token.Active = true; token.Active = true;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
var res = new Auth.OauthTokenResponse { var res = new AuthSchemas.OauthTokenResponse {
CreatedAt = token.CreatedAt, CreatedAt = token.CreatedAt,
Scopes = token.Scopes, Scopes = token.Scopes,
AccessToken = token.Token AccessToken = token.Token

View file

@ -1,5 +1,6 @@
using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;

View file

@ -0,0 +1,45 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.MfmSharp.Conversion;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
public class NoteRenderer(IOptions<Config.InstanceSection> config, UserRenderer userRenderer) {
public async Task<Status> RenderAsync(Note note, int recurse = 2) {
var uri = note.Uri ?? $"https://{config.Value.WebDomain}/notes/@{note.Id}";
var renote = note.Renote != null && recurse > 0 ? await RenderAsync(note.Renote, --recurse) : null;
var text = note.Text; //TODO: append quote uri
var content = text != null ? await MfmConverter.ToHtmlAsync(text) : null;
var res = new Status {
Id = note.Id,
Uri = uri,
Url = note.Url ?? uri,
Account = await userRenderer.RenderAsync(note.User), //TODO: batch this
ReplyId = note.ReplyId,
ReplyUserId = note.ReplyUserId,
Renote = renote, //TODO: check if it's a pure renote
Quote = renote, //TODO: see above
ContentType = "text/x.misskeymarkdown",
CreatedAt = note.CreatedAt.ToString("O")[..^5],
EditedAt = note.UpdatedAt?.ToString("O")[..^5],
RepliesCount = note.RepliesCount,
RenoteCount = note.RenoteCount,
FavoriteCount = 0, //FIXME
IsRenoted = false, //FIXME
IsFavorited = false, //FIXME
IsBookmarked = false, //FIXME
IsMuted = null, //FIXME
IsSensitive = note.Cw != null,
ContentWarning = note.Cw ?? "",
Visibility = Status.EncodeVisibility(note.Visibility),
Content = content,
Text = text,
IsPinned = false //FIXME
};
return res;
}
}

View file

@ -1,4 +1,4 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
@ -37,7 +37,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
HeaderStaticUrl = user.BannerUrl ?? _transparent, //TODO HeaderStaticUrl = user.BannerUrl ?? _transparent, //TODO
MovedToAccount = null, //TODO MovedToAccount = null, //TODO
IsBot = user.IsBot, IsBot = user.IsBot,
IsDiscoverable = user.IsExplorable, IsDiscoverable = user.IsExplorable
}; };
return res; return res;

View file

@ -7,7 +7,7 @@ using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas; namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
public abstract class Auth { public abstract class AuthSchemas {
public class VerifyAppCredentialsResponse { public class VerifyAppCredentialsResponse {
public required OauthApp App; public required OauthApp App;

View file

@ -1,11 +1,6 @@
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Helpers;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
using JC = System.Text.Json.Serialization.JsonConverterAttribute;
using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas; namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
public class Account { public class Account {
[J("id")] public required string Id { get; set; } [J("id")] public required string Id { get; set; }
@ -28,7 +23,7 @@ public class Account {
[J("bot")] public required bool IsBot { get; set; } [J("bot")] public required bool IsBot { get; set; }
[J("discoverable")] public required bool IsDiscoverable { get; set; } [J("discoverable")] public required bool IsDiscoverable { get; set; }
[J("source")] public string? Source => null; //FIXME [J("source")] public object? Source => null; //FIXME
[J("fields")] public IEnumerable<string> Fields => []; //FIXME [J("fields")] public object[] Fields => []; //FIXME
[J("emojis")] public IEnumerable<string> Emoji => []; //FIXME [J("emojis")] public object[] Emoji => []; //FIXME
} }

View file

@ -0,0 +1,75 @@
using System.Text.Json.Serialization;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Middleware;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
public class Status {
[J("text")] [JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
public required string? Text;
[J("id")] public required string Id { get; set; }
[J("uri")] public required string Uri { get; set; }
[J("url")] public required string Url { get; set; }
[J("account")] public required Account Account { get; set; }
[J("in_reply_to_id")] public required string? ReplyId { get; set; }
[J("in_reply_to_account_id")] public required string? ReplyUserId { get; set; }
[J("reblog")] public required Status? Renote { get; set; }
[J("quote")] public required Status? Quote { get; set; }
[J("content_type")] public required string ContentType { get; set; }
[J("created_at")] public required string CreatedAt { get; set; }
[J("edited_at")] public required string? EditedAt { get; set; }
[J("replies_count")] public required long RepliesCount { get; set; }
[J("reblogs_count")] public required long RenoteCount { get; set; }
[J("favourites_count")] public required long FavoriteCount { get; set; }
[J("reblogged")] public required bool? IsRenoted { get; set; }
[J("favourited")] public required bool? IsFavorited { get; set; }
[J("bookmarked")] public required bool? IsBookmarked { get; set; }
[J("muted")] public required bool? IsMuted { get; set; }
[J("sensitive")] public required bool IsSensitive { get; set; }
[J("spoiler_text")] public required string ContentWarning { get; set; }
[J("visibility")] public required string Visibility { get; set; }
[J("content")]
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
public required string? Content { get; set; }
[J("pinned")]
[JI(Condition = JsonIgnoreCondition.WhenWritingNull)]
public required bool? IsPinned { get; set; }
[J("emojis")] public object[] Emojis => []; //FIXME
[J("media_attachments")] public object[] Media => []; //FIXME
[J("mentions")] public object[] Mentions => []; //FIXME
[J("tags")] public object[] Tags => []; //FIXME
[J("reactions")] public object[] Reactions => []; //FIXME
[J("filtered")] public object[] Filtered => []; //FIXME
[J("card")] public object? Card => null; //FIXME
[J("poll")] public object? Poll => null; //FIXME
[J("application")] public object? Application => null; //FIXME
[J("language")] public string? Language => null; //FIXME
public static string EncodeVisibility(Note.NoteVisibility visibility) {
return visibility switch {
Note.NoteVisibility.Public => "public",
Note.NoteVisibility.Home => "unlisted",
Note.NoteVisibility.Followers => "private",
Note.NoteVisibility.Specified => "direct",
Note.NoteVisibility.Hidden => throw new GracefulException("Cannot encode hidden visibility"), //FIXME
_ => throw new GracefulException($"Unknown visibility: {visibility}")
};
}
public static Note.NoteVisibility DecodeVisibility(string visibility) {
return visibility switch {
"public" => Note.NoteVisibility.Public,
"unlisted" => Note.NoteVisibility.Home,
"private" => Note.NoteVisibility.Followers,
"direct" => Note.NoteVisibility.Specified,
_ => throw GracefulException.BadRequest($"Unknown visibility: {visibility}")
};
}
}

View file

@ -36,6 +36,7 @@ public static class ServiceExtensions {
//TODO: make this prettier //TODO: make this prettier
services.AddScoped<Controllers.Mastodon.Renderers.UserRenderer>(); services.AddScoped<Controllers.Mastodon.Renderers.UserRenderer>();
services.AddScoped<Controllers.Mastodon.Renderers.NoteRenderer>();
// Singleton = instantiated once across application lifetime // Singleton = instantiated once across application lifetime
services.AddSingleton<HttpClient>(); services.AddSingleton<HttpClient>();