[backend/masto-client] Add schemas and renderers for user statuses
This commit is contained in:
parent
09f9d2395c
commit
aa6988399d
8 changed files with 139 additions and 22 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>();
|
||||||
|
|
Loading…
Add table
Reference in a new issue