[backend/asp] Refactor controllers
This commit aims to improve readability of MVC controllers & actions. The main change is the switch to custom [ProducesResults] and [ProducesErrors] attributes.
This commit is contained in:
parent
5a8295e4f7
commit
0776a50cbe
40 changed files with 1246 additions and 1097 deletions
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
|
@ -9,17 +10,17 @@ using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
|||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Queues;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Federation;
|
||||
|
||||
[FederationApiController]
|
||||
[FederationSemaphore]
|
||||
[UseNewtonsoftJson]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
[ProducesActivityStreamsPayload]
|
||||
public class ActivityPubController(
|
||||
DatabaseContext db,
|
||||
QueueService queues,
|
||||
|
@ -31,71 +32,78 @@ public class ActivityPubController(
|
|||
[HttpGet("/notes/{id}")]
|
||||
[AuthorizedFetch]
|
||||
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASNote))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetNote(string id)
|
||||
[OverrideResultType<ASNote>]
|
||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.MovedPermanently)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ActionResult<JObject>> GetNote(string id)
|
||||
{
|
||||
var actor = HttpContext.GetActor();
|
||||
var note = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.EnsureVisibleFor(actor)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (note == null) return NotFound();
|
||||
if (note == null) throw GracefulException.NotFound("Note not found");
|
||||
if (note.User.IsRemoteUser)
|
||||
return RedirectPermanent(note.Uri ?? throw new Exception("Refusing to render remote note without uri"));
|
||||
var rendered = await noteRenderer.RenderAsync(note);
|
||||
var compacted = rendered.Compact();
|
||||
return Ok(compacted);
|
||||
var rendered = await noteRenderer.RenderAsync(note);
|
||||
return rendered.Compact();
|
||||
}
|
||||
|
||||
[HttpGet("/notes/{id}/activity")]
|
||||
[AuthorizedFetch]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActivity))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetRenote(string id)
|
||||
[OverrideResultType<ASAnnounce>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<JObject> GetRenote(string id)
|
||||
{
|
||||
var actor = HttpContext.GetActor();
|
||||
|
||||
var note = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.EnsureVisibleFor(actor)
|
||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserHost == null);
|
||||
.Where(p => p.Id == id && p.UserHost == null && p.IsPureRenote && p.Renote != null)
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.NotFound("Note not found");
|
||||
|
||||
if (note is not { IsPureRenote: true, Renote: not null }) return NotFound();
|
||||
|
||||
var rendered = ActivityPub.ActivityRenderer.RenderAnnounce(noteRenderer.RenderLite(note.Renote),
|
||||
note.GetPublicUri(config.Value),
|
||||
userRenderer.RenderLite(note.User),
|
||||
note.Visibility,
|
||||
note.User.GetPublicUri(config.Value) + "/followers");
|
||||
var compacted = rendered.Compact();
|
||||
return Ok(compacted);
|
||||
return ActivityPub.ActivityRenderer
|
||||
.RenderAnnounce(noteRenderer.RenderLite(note.Renote!),
|
||||
note.GetPublicUri(config.Value),
|
||||
userRenderer.RenderLite(note.User),
|
||||
note.Visibility,
|
||||
note.User.GetPublicUri(config.Value) + "/followers")
|
||||
.Compact();
|
||||
}
|
||||
|
||||
[HttpGet("/users/{id}")]
|
||||
[AuthorizedFetch]
|
||||
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActor))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetUser(string id)
|
||||
[OverrideResultType<ASActor>]
|
||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.MovedPermanently)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ActionResult<JObject>> GetUser(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (user == null) return NotFound();
|
||||
if (user.IsRemoteUser) return user.Uri != null ? RedirectPermanent(user.Uri) : NotFound();
|
||||
var rendered = await userRenderer.RenderAsync(user);
|
||||
var compacted = LdHelpers.Compact(rendered);
|
||||
return Ok(compacted);
|
||||
if (user == null) throw GracefulException.NotFound("User not found");
|
||||
if (user.IsRemoteUser)
|
||||
{
|
||||
if (user.Uri != null)
|
||||
return RedirectPermanent(user.Uri);
|
||||
throw GracefulException.NotFound("User not found");
|
||||
}
|
||||
|
||||
var rendered = await userRenderer.RenderAsync(user);
|
||||
return ((ASObject)rendered).Compact();
|
||||
}
|
||||
|
||||
[HttpGet("/users/{id}/collections/featured")]
|
||||
[AuthorizedFetch]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActor))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetUserFeatured(string id)
|
||||
[OverrideResultType<ASOrderedCollection>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<JObject> GetUserFeatured(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && p.IsLocalUser);
|
||||
if (user == null) return NotFound();
|
||||
if (user == null) throw GracefulException.NotFound("User not found");
|
||||
|
||||
var pins = await db.UserNotePins.Where(p => p.User == user)
|
||||
.OrderByDescending(p => p.Id)
|
||||
|
@ -114,65 +122,72 @@ public class ActivityPubController(
|
|||
Items = rendered.Cast<ASObject>().ToList()
|
||||
};
|
||||
|
||||
var compacted = res.Compact();
|
||||
return Ok(compacted);
|
||||
return res.Compact();
|
||||
}
|
||||
|
||||
[HttpGet("/@{acct}")]
|
||||
[AuthorizedFetch]
|
||||
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActor))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetUserByUsername(string acct)
|
||||
[OverrideResultType<ASActor>]
|
||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.MovedPermanently)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ActionResult<JObject>> GetUserByUsername(string acct)
|
||||
{
|
||||
var split = acct.Split('@');
|
||||
if (acct.Split('@').Length > 2) return NotFound();
|
||||
if (acct.Split('@').Length > 2) throw GracefulException.NotFound("User not found");
|
||||
if (split.Length == 2)
|
||||
{
|
||||
var remoteUser = await db.Users.IncludeCommonProperties()
|
||||
var remoteUser = await db.Users
|
||||
.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.UsernameLower == split[0].ToLowerInvariant() &&
|
||||
p.Host == split[1].ToLowerInvariant().ToPunycode());
|
||||
return remoteUser?.Uri != null ? RedirectPermanent(remoteUser.Uri) : NotFound();
|
||||
|
||||
if (remoteUser?.Uri != null)
|
||||
return RedirectPermanent(remoteUser.Uri);
|
||||
throw GracefulException.NotFound("User not found");
|
||||
}
|
||||
|
||||
var user = await db.Users.IncludeCommonProperties()
|
||||
var user = await db.Users
|
||||
.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.UsernameLower == acct.ToLowerInvariant() && p.IsLocalUser);
|
||||
if (user == null) return NotFound();
|
||||
var rendered = await userRenderer.RenderAsync(user);
|
||||
var compacted = LdHelpers.Compact(rendered);
|
||||
return Ok(compacted);
|
||||
|
||||
if (user == null) throw GracefulException.NotFound("User not found");
|
||||
var rendered = await userRenderer.RenderAsync(user);
|
||||
return ((ASObject)rendered).Compact();
|
||||
}
|
||||
|
||||
[HttpPost("/inbox")]
|
||||
[HttpPost("/users/{id}/inbox")]
|
||||
[InboxValidation]
|
||||
[EnableRequestBuffering(1024 * 1024)]
|
||||
[Produces("text/plain")]
|
||||
[Consumes("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
public async Task<IActionResult> Inbox(string? id)
|
||||
[ConsumesActivityStreamsPayload]
|
||||
[ProducesResults(HttpStatusCode.Accepted)]
|
||||
public async Task<AcceptedResult> Inbox(string? id)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8, true, 1024, true);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
Request.Body.Position = 0;
|
||||
|
||||
await queues.InboxQueue.EnqueueAsync(new InboxJobData
|
||||
{
|
||||
Body = body,
|
||||
InboxUserId = id,
|
||||
AuthenticatedUserId = HttpContext.GetActor()?.Id
|
||||
});
|
||||
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[HttpGet("/emoji/{name}")]
|
||||
[AuthorizedFetch]
|
||||
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASEmoji))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetEmoji(string name)
|
||||
[OverrideResultType<ASEmoji>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ActionResult<JObject>> GetEmoji(string name)
|
||||
{
|
||||
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Name == name && p.Host == null);
|
||||
if (emoji == null) return NotFound();
|
||||
if (emoji == null) throw GracefulException.NotFound("Emoji not found");
|
||||
|
||||
var rendered = new ASEmoji
|
||||
{
|
||||
|
@ -181,7 +196,6 @@ public class ActivityPubController(
|
|||
Image = new ASImage { Url = new ASLink(emoji.PublicUrl) }
|
||||
};
|
||||
|
||||
var compacted = LdHelpers.Compact(rendered);
|
||||
return Ok(compacted);
|
||||
return LdHelpers.Compact(rendered);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||
|
@ -18,8 +20,8 @@ public class NodeInfoController(IOptions<Config.InstanceSection> config, Databas
|
|||
{
|
||||
[HttpGet("2.1")]
|
||||
[HttpGet("2.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(WebFingerResponse))]
|
||||
public async Task<IActionResult> GetNodeInfo()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<NodeInfoResponse> GetNodeInfo()
|
||||
{
|
||||
var cutoffMonth = DateTime.UtcNow - TimeSpan.FromDays(30);
|
||||
var cutoffHalfYear = DateTime.UtcNow - TimeSpan.FromDays(180);
|
||||
|
@ -36,7 +38,7 @@ public class NodeInfoController(IOptions<Config.InstanceSection> config, Databas
|
|||
p.LastActiveDate > cutoffHalfYear);
|
||||
var localPosts = await db.Notes.LongCountAsync(p => p.UserHost == null);
|
||||
|
||||
var result = new NodeInfoResponse
|
||||
return new NodeInfoResponse
|
||||
{
|
||||
Version = Request.Path.Value?.EndsWith("2.1") ?? false ? "2.1" : "2.0",
|
||||
Software = new NodeInfoResponse.NodeInfoSoftware
|
||||
|
@ -90,7 +92,5 @@ public class NodeInfoController(IOptions<Config.InstanceSection> config, Databas
|
|||
},
|
||||
OpenRegistrations = false
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,16 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -23,9 +25,9 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
{
|
||||
[HttpGet("webfinger")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(WebFingerResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> WebFinger([FromQuery] string resource)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<WebFingerResponse> WebFinger([FromQuery] string resource)
|
||||
{
|
||||
User? user;
|
||||
if (resource.StartsWith($"https://{config.Value.WebDomain}/users/"))
|
||||
|
@ -39,20 +41,20 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
resource = resource[5..];
|
||||
|
||||
var split = resource.TrimStart('@').Split('@');
|
||||
if (split.Length > 2) return NotFound();
|
||||
if (split.Length > 2) throw GracefulException.NotFound("User not found");
|
||||
if (split.Length == 2)
|
||||
{
|
||||
List<string> domains = [config.Value.AccountDomain, config.Value.WebDomain];
|
||||
if (!domains.Contains(split[1])) return NotFound();
|
||||
if (!domains.Contains(split[1])) throw GracefulException.NotFound("User not found");
|
||||
}
|
||||
|
||||
user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == split[0].ToLowerInvariant() &&
|
||||
p.IsLocalUser);
|
||||
}
|
||||
|
||||
if (user == null) return NotFound();
|
||||
if (user == null) throw GracefulException.NotFound("User not found");
|
||||
|
||||
var response = new WebFingerResponse
|
||||
return new WebFingerResponse
|
||||
{
|
||||
Subject = $"acct:{user.Username}@{config.Value.AccountDomain}",
|
||||
Links =
|
||||
|
@ -76,16 +78,14 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
}
|
||||
]
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("nodeinfo")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NodeInfoIndexResponse))]
|
||||
public IActionResult NodeInfo()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public NodeInfoIndexResponse NodeInfo()
|
||||
{
|
||||
var response = new NodeInfoIndexResponse
|
||||
return new NodeInfoIndexResponse
|
||||
{
|
||||
Links =
|
||||
[
|
||||
|
@ -101,14 +101,12 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
}
|
||||
]
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("host-meta")]
|
||||
[Produces("application/xrd+xml", "application/jrd+json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(HostMetaJsonResponse))]
|
||||
public IActionResult HostMeta()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public ActionResult<HostMetaJsonResponse> HostMeta()
|
||||
{
|
||||
var accept = Request.Headers.Accept.OfType<string>()
|
||||
.SelectMany(p => p.Split(","))
|
||||
|
@ -117,7 +115,7 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
.ToList();
|
||||
|
||||
if (accept.Contains("application/jrd+json") || accept.Contains("application/json"))
|
||||
return HostMetaJson();
|
||||
return Ok(HostMetaJson());
|
||||
|
||||
var obj = new HostMetaXmlResponse(config.Value.WebDomain);
|
||||
var serializer = new XmlSerializer(obj.GetType());
|
||||
|
@ -129,10 +127,10 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
|
||||
[HttpGet("host-meta.json")]
|
||||
[Produces("application/jrd+json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(HostMetaJsonResponse))]
|
||||
public IActionResult HostMetaJson()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public HostMetaJsonResponse HostMetaJson()
|
||||
{
|
||||
return Ok(new HostMetaJsonResponse(config.Value.WebDomain));
|
||||
return new HostMetaJsonResponse(config.Value.WebDomain);
|
||||
}
|
||||
|
||||
private class Utf8StringWriter : StringWriter
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
|
@ -37,18 +38,17 @@ public class AccountController(
|
|||
{
|
||||
[HttpGet("verify_credentials")]
|
||||
[Authorize("read:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
public async Task<IActionResult> VerifyUserCredentials()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<AccountEntity> VerifyUserCredentials()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var res = await userRenderer.RenderAsync(user, user.UserProfile, source: true);
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderAsync(user, user.UserProfile, source: true);
|
||||
}
|
||||
|
||||
[HttpPatch("update_credentials")]
|
||||
[Authorize("write:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
public async Task<IActionResult> UpdateUserCredentials([FromHybrid] AccountSchemas.AccountUpdateRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<AccountEntity> UpdateUserCredentials([FromHybrid] AccountSchemas.AccountUpdateRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.UserProfile == null)
|
||||
|
@ -124,15 +124,13 @@ public class AccountController(
|
|||
}
|
||||
|
||||
user = await userSvc.UpdateLocalUserAsync(user, prevAvatarId, prevBannerId);
|
||||
|
||||
var res = await userRenderer.RenderAsync(user, user.UserProfile, source: true);
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderAsync(user, user.UserProfile, source: true);
|
||||
}
|
||||
|
||||
[HttpDelete("/api/v1/profile/avatar")]
|
||||
[Authorize("write:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
public async Task<IActionResult> DeleteUserAvatar()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<AccountEntity> DeleteUserAvatar()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.AvatarId != null)
|
||||
|
@ -153,8 +151,8 @@ public class AccountController(
|
|||
|
||||
[HttpDelete("/api/v1/profile/header")]
|
||||
[Authorize("write:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
public async Task<IActionResult> DeleteUserBanner()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<AccountEntity> DeleteUserBanner()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.BannerId != null)
|
||||
|
@ -174,9 +172,9 @@ public class AccountController(
|
|||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<AccountEntity> GetUser(string id)
|
||||
{
|
||||
var localUser = HttpContext.GetUser();
|
||||
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && localUser == null)
|
||||
|
@ -188,16 +186,15 @@ public class AccountController(
|
|||
if (config.Value.PublicPreview <= Enums.PublicPreview.Restricted && user.IsRemoteUser && localUser == null)
|
||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||
|
||||
var res = await userRenderer.RenderAsync(await userResolver.GetUpdatedUser(user));
|
||||
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderAsync(await userResolver.GetUpdatedUser(user));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/follow")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
//TODO: [FromHybrid] request (bool reblogs, bool notify, bool languages)
|
||||
public async Task<IActionResult> FollowUser(string id)
|
||||
public async Task<RelationshipEntity> FollowUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -222,15 +219,14 @@ public class AccountController(
|
|||
followee.PrecomputedIsFollowedBy = true;
|
||||
}
|
||||
|
||||
var res = RenderRelationship(followee);
|
||||
|
||||
return Ok(res);
|
||||
return RenderRelationship(followee);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unfollow")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
public async Task<IActionResult> UnfollowUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<RelationshipEntity> UnfollowUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -244,16 +240,14 @@ public class AccountController(
|
|||
throw GracefulException.RecordNotFound();
|
||||
|
||||
await userSvc.UnfollowUserAsync(user, followee);
|
||||
|
||||
var res = RenderRelationship(followee);
|
||||
|
||||
return Ok(res);
|
||||
return RenderRelationship(followee);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/mute")]
|
||||
[Authorize("write:mutes")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
public async Task<IActionResult> MuteUser(string id, [FromHybrid] AccountSchemas.AccountMuteRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<RelationshipEntity> MuteUser(string id, [FromHybrid] AccountSchemas.AccountMuteRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -269,16 +263,14 @@ public class AccountController(
|
|||
//TODO: handle notifications parameter
|
||||
DateTime? expiration = request.Duration == 0 ? null : DateTime.UtcNow + TimeSpan.FromSeconds(request.Duration);
|
||||
await userSvc.MuteUserAsync(user, mutee, expiration);
|
||||
|
||||
var res = RenderRelationship(mutee);
|
||||
|
||||
return Ok(res);
|
||||
return RenderRelationship(mutee);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unmute")]
|
||||
[Authorize("write:mutes")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
public async Task<IActionResult> UnmuteUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<RelationshipEntity> UnmuteUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -292,16 +284,14 @@ public class AccountController(
|
|||
throw GracefulException.RecordNotFound();
|
||||
|
||||
await userSvc.UnmuteUserAsync(user, mutee);
|
||||
|
||||
var res = RenderRelationship(mutee);
|
||||
|
||||
return Ok(res);
|
||||
return RenderRelationship(mutee);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/block")]
|
||||
[Authorize("write:blocks")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
public async Task<IActionResult> BlockUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<RelationshipEntity> BlockUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -315,16 +305,14 @@ public class AccountController(
|
|||
throw GracefulException.RecordNotFound();
|
||||
|
||||
await userSvc.BlockUserAsync(user, blockee);
|
||||
|
||||
var res = RenderRelationship(blockee);
|
||||
|
||||
return Ok(res);
|
||||
return RenderRelationship(blockee);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unblock")]
|
||||
[Authorize("write:blocks")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
public async Task<IActionResult> UnblockUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<RelationshipEntity> UnblockUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -338,16 +326,13 @@ public class AccountController(
|
|||
throw GracefulException.RecordNotFound();
|
||||
|
||||
await userSvc.UnblockUserAsync(user, blockee);
|
||||
|
||||
var res = RenderRelationship(blockee);
|
||||
|
||||
return Ok(res);
|
||||
return RenderRelationship(blockee);
|
||||
}
|
||||
|
||||
[HttpGet("relationships")]
|
||||
[Authorize("read:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity[]))]
|
||||
public async Task<IActionResult> GetRelationships([FromQuery(Name = "id")] List<string> ids)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<RelationshipEntity>> GetRelationships([FromQuery(Name = "id")] List<string> ids)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
|
@ -357,41 +342,38 @@ public class AccountController(
|
|||
.PrecomputeRelationshipData(user)
|
||||
.ToListAsync();
|
||||
|
||||
var res = users.Select(RenderRelationship);
|
||||
|
||||
return Ok(res);
|
||||
return users.Select(RenderRelationship);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/statuses")]
|
||||
[Authorize("read:statuses")]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
public async Task<IActionResult> GetUserStatuses(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<StatusEntity>> GetUserStatuses(
|
||||
string id, AccountSchemas.AccountStatusesRequest request, MastodonPaginationQuery query
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var account = await db.Users.FirstOrDefaultAsync(p => p.Id == id) ?? throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.FilterByUser(account)
|
||||
.FilterByAccountStatusesRequest(request)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db, except: id)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Accounts);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.FilterByUser(account)
|
||||
.FilterByAccountStatusesRequest(request)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db, except: id)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Accounts);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/followers")]
|
||||
[Authenticate("read:accounts")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetUserFollowers(string id, MastodonPaginationQuery query)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<AccountEntity>> GetUserFollowers(string id, MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -408,28 +390,26 @@ public class AccountController(
|
|||
if (user == null || user.Id != account.Id)
|
||||
{
|
||||
if (account.UserProfile?.FFVisibility == UserProfile.UserProfileFFVisibility.Private)
|
||||
return Ok((List<AccountEntity>) []);
|
||||
return [];
|
||||
if (account.UserProfile?.FFVisibility == UserProfile.UserProfileFFVisibility.Followers)
|
||||
if (user == null || !await db.Users.AnyAsync(p => p == account && p.Followers.Contains(user)))
|
||||
return Ok((List<AccountEntity>) []);
|
||||
return [];
|
||||
}
|
||||
|
||||
var res = await db.Users
|
||||
.Where(p => p == account)
|
||||
.SelectMany(p => p.Followers)
|
||||
.IncludeCommonProperties()
|
||||
.Paginate(query, ControllerContext)
|
||||
.RenderAllForMastodonAsync(userRenderer);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Users
|
||||
.Where(p => p == account)
|
||||
.SelectMany(p => p.Followers)
|
||||
.IncludeCommonProperties()
|
||||
.Paginate(query, ControllerContext)
|
||||
.RenderAllForMastodonAsync(userRenderer);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/following")]
|
||||
[Authenticate("read:accounts")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetUserFollowing(string id, MastodonPaginationQuery query)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<AccountEntity>> GetUserFollowing(string id, MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (config.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -446,41 +426,38 @@ public class AccountController(
|
|||
if (user == null || user.Id != account.Id)
|
||||
{
|
||||
if (account.UserProfile?.FFVisibility == UserProfile.UserProfileFFVisibility.Private)
|
||||
return Ok((List<AccountEntity>) []);
|
||||
return [];
|
||||
if (account.UserProfile?.FFVisibility == UserProfile.UserProfileFFVisibility.Followers)
|
||||
if (user == null || !await db.Users.AnyAsync(p => p == account && p.Followers.Contains(user)))
|
||||
return Ok((List<AccountEntity>) []);
|
||||
return [];
|
||||
}
|
||||
|
||||
var res = await db.Users
|
||||
.Where(p => p == account)
|
||||
.SelectMany(p => p.Following)
|
||||
.IncludeCommonProperties()
|
||||
.Paginate(query, ControllerContext)
|
||||
.RenderAllForMastodonAsync(userRenderer);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Users
|
||||
.Where(p => p == account)
|
||||
.SelectMany(p => p.Following)
|
||||
.IncludeCommonProperties()
|
||||
.Paginate(query, ControllerContext)
|
||||
.RenderAllForMastodonAsync(userRenderer);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/featured_tags")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<object>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetUserFeaturedTags(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<object>> GetUserFeaturedTags(string id)
|
||||
{
|
||||
_ = await db.Users
|
||||
.Include(p => p.UserProfile)
|
||||
.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = Array.Empty<object>();
|
||||
return Ok(res);
|
||||
return [];
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/follow_requests")]
|
||||
[Authorize("read:follows")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
public async Task<IActionResult> GetFollowRequests(MastodonPaginationQuery query)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<AccountEntity>> GetFollowRequests(MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var requests = await db.FollowRequests
|
||||
|
@ -491,17 +468,15 @@ public class AccountController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(requests);
|
||||
var res = await userRenderer.RenderManyAsync(requests.Select(p => p.Entity));
|
||||
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderManyAsync(requests.Select(p => p.Entity));
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/favourites")]
|
||||
[Authorize("read:favourites")]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> GetLikedNotes(MastodonPaginationQuery query)
|
||||
public async Task<IEnumerable<StatusEntity>> GetLikedNotes(MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var likes = await db.NoteLikes
|
||||
|
@ -515,16 +490,15 @@ public class AccountController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(likes);
|
||||
var res = await noteRenderer.RenderManyAsync(likes.Select(p => p.Entity).EnforceRenoteReplyVisibility(), user);
|
||||
return Ok(res);
|
||||
return await noteRenderer.RenderManyAsync(likes.Select(p => p.Entity).EnforceRenoteReplyVisibility(), user);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/bookmarks")]
|
||||
[Authorize("read:bookmarks")]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> GetBookmarkedNotes(MastodonPaginationQuery query)
|
||||
public async Task<IEnumerable<StatusEntity>> GetBookmarkedNotes(MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var bookmarks = await db.NoteBookmarks
|
||||
|
@ -538,17 +512,15 @@ public class AccountController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(bookmarks);
|
||||
var res =
|
||||
await noteRenderer.RenderManyAsync(bookmarks.Select(p => p.Entity).EnforceRenoteReplyVisibility(), user);
|
||||
return Ok(res);
|
||||
return await noteRenderer.RenderManyAsync(bookmarks.Select(p => p.Entity).EnforceRenoteReplyVisibility(), user);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/blocks")]
|
||||
[Authorize("read:blocks")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> GetBlockedUsers(MastodonPaginationQuery pq)
|
||||
public async Task<IEnumerable<AccountEntity>> GetBlockedUsers(MastodonPaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var blocks = await db.Blockings
|
||||
|
@ -559,17 +531,15 @@ public class AccountController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(blocks);
|
||||
var res = await userRenderer.RenderManyAsync(blocks.Select(p => p.Entity));
|
||||
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderManyAsync(blocks.Select(p => p.Entity));
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/mutes")]
|
||||
[Authorize("read:mutes")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> GetMutedUsers(MastodonPaginationQuery pq)
|
||||
public async Task<IEnumerable<AccountEntity>> GetMutedUsers(MastodonPaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var mutes = await db.Mutings
|
||||
|
@ -580,16 +550,14 @@ public class AccountController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(mutes);
|
||||
var res = await userRenderer.RenderManyAsync(mutes.Select(p => p.Entity));
|
||||
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderManyAsync(mutes.Select(p => p.Entity));
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/follow_requests/{id}/authorize")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> AcceptFollowRequest(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<RelationshipEntity> AcceptFollowRequest(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var request = await db.FollowRequests.Where(p => p.Followee == user && p.FollowerId == id)
|
||||
|
@ -600,23 +568,19 @@ public class AccountController(
|
|||
if (request != null)
|
||||
await userSvc.AcceptFollowRequestAsync(request);
|
||||
|
||||
var relationship = await db.Users.Where(p => id == p.Id)
|
||||
.IncludeCommonProperties()
|
||||
.PrecomputeRelationshipData(user)
|
||||
.Select(u => RenderRelationship(u))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (relationship == null)
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(relationship);
|
||||
return await db.Users.Where(p => id == p.Id)
|
||||
.IncludeCommonProperties()
|
||||
.PrecomputeRelationshipData(user)
|
||||
.Select(u => RenderRelationship(u))
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/follow_requests/{id}/reject")]
|
||||
[Authorize("write:follows")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> RejectFollowRequest(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<RelationshipEntity> RejectFollowRequest(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var request = await db.FollowRequests.Where(p => p.Followee == user && p.FollowerId == id)
|
||||
|
@ -627,26 +591,21 @@ public class AccountController(
|
|||
if (request != null)
|
||||
await userSvc.RejectFollowRequestAsync(request);
|
||||
|
||||
var relationship = await db.Users.Where(p => id == p.Id)
|
||||
.IncludeCommonProperties()
|
||||
.PrecomputeRelationshipData(user)
|
||||
.Select(u => RenderRelationship(u))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (relationship == null)
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(relationship);
|
||||
return await db.Users.Where(p => id == p.Id)
|
||||
.IncludeCommonProperties()
|
||||
.PrecomputeRelationshipData(user)
|
||||
.Select(u => RenderRelationship(u))
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
}
|
||||
|
||||
[HttpGet("lookup")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AccountEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> LookupUser([FromQuery] string acct)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<AccountEntity> LookupUser([FromQuery] string acct)
|
||||
{
|
||||
var user = await userResolver.LookupAsync(acct) ?? throw GracefulException.RecordNotFound();
|
||||
var res = await userRenderer.RenderAsync(user);
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderAsync(user);
|
||||
}
|
||||
|
||||
private static RelationshipEntity RenderRelationship(User u)
|
||||
|
|
|
@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -27,17 +27,17 @@ public class AnnouncementController(DatabaseContext db, MfmConverter mfmConverte
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AnnouncementEntity>))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> GetAnnouncements([FromQuery(Name = "with_dismissed")] bool withDismissed)
|
||||
public async Task<IEnumerable<AnnouncementEntity>> GetAnnouncements(
|
||||
[FromQuery(Name = "with_dismissed")] bool withDismissed
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var announcements = db.Announcements.AsQueryable();
|
||||
|
||||
if (!withDismissed)
|
||||
{
|
||||
announcements = announcements.Where(p => p.IsReadBy(user));
|
||||
}
|
||||
|
||||
var res = await announcements.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt)
|
||||
.Select(p => new AnnouncementEntity
|
||||
|
@ -56,20 +56,20 @@ public class AnnouncementController(DatabaseContext db, MfmConverter mfmConverte
|
|||
.ToListAsync();
|
||||
|
||||
await res.Select(async p => p.Content = await mfmConverter.ToHtmlAsync(p.Content, [], null)).AwaitAllAsync();
|
||||
|
||||
return Ok(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/dismiss")]
|
||||
[Authorize("write:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> DismissAnnouncement(string id)
|
||||
public async Task<object> DismissAnnouncement(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var announcement = await db.Announcements.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
throw GracefulException.NotFound("Announcement not found");
|
||||
|
||||
if (await db.Announcements.AnyAsync(p => p == announcement && !p.IsReadBy(user)))
|
||||
{
|
||||
|
@ -84,19 +84,19 @@ public class AnnouncementController(DatabaseContext db, MfmConverter mfmConverte
|
|||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
|
||||
[HttpPut("{id}/reactions/{name}")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesErrors(HttpStatusCode.NotImplemented)]
|
||||
public IActionResult ReactToAnnouncement(string id, string name) =>
|
||||
throw new GracefulException(HttpStatusCode.NotImplemented,
|
||||
"Iceshrimp.NET does not support this endpoint due to database schema differences to Mastodon");
|
||||
|
||||
[HttpDelete("{id}/reactions/{name}")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesErrors(HttpStatusCode.NotImplemented)]
|
||||
public IActionResult RemoveAnnouncementReaction(string id, string name) =>
|
||||
throw new GracefulException(HttpStatusCode.NotImplemented,
|
||||
"Iceshrimp.NET does not support this endpoint due to database schema differences to Mastodon");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Cors;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Iceshrimp.Backend.Controllers.Mastodon.Schemas.AuthSchemas;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||
|
||||
|
@ -22,27 +24,24 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
|
|||
{
|
||||
[HttpGet("/api/v1/apps/verify_credentials")]
|
||||
[Authenticate]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.VerifyAppCredentialsResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> VerifyAppCredentials()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Unauthorized)]
|
||||
public async Task<VerifyAppCredentialsResponse> VerifyAppCredentials()
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var token = HttpContext.GetOauthToken() ?? throw GracefulException.Unauthorized("The access token is invalid");
|
||||
|
||||
var res = new AuthSchemas.VerifyAppCredentialsResponse
|
||||
return new VerifyAppCredentialsResponse
|
||||
{
|
||||
App = token.App, VapidKey = await meta.Get(MetaEntity.VapidPublicKey)
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/apps")]
|
||||
[EnableRateLimiting("auth")]
|
||||
[ConsumesHybrid]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.RegisterAppResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> RegisterApp([FromHybrid] AuthSchemas.RegisterAppRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<RegisterAppResponse> RegisterApp([FromHybrid] RegisterAppRequest request)
|
||||
{
|
||||
if (request.RedirectUris.Count == 0)
|
||||
throw GracefulException.BadRequest("Invalid redirect_uris parameter");
|
||||
|
@ -79,19 +78,14 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
|
|||
await db.AddAsync(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = new AuthSchemas.RegisterAppResponse
|
||||
{
|
||||
App = app, VapidKey = await meta.Get(MetaEntity.VapidPublicKey)
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
return new RegisterAppResponse { App = app, VapidKey = await meta.Get(MetaEntity.VapidPublicKey) };
|
||||
}
|
||||
|
||||
[HttpPost("/oauth/token")]
|
||||
[ConsumesHybrid]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.OauthTokenResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetOauthToken([FromHybrid] AuthSchemas.OauthTokenRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<OauthTokenResponse> GetOauthToken([FromHybrid] OauthTokenRequest request)
|
||||
{
|
||||
//TODO: app-level access (grant_type = "client_credentials")
|
||||
if (request.GrantType != "authorization_code")
|
||||
|
@ -99,49 +93,46 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
|
|||
var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Code == request.Code &&
|
||||
p.App.ClientId == request.ClientId &&
|
||||
p.App.ClientSecret == request.ClientSecret);
|
||||
// @formatter:off
|
||||
if (token == null)
|
||||
throw GracefulException
|
||||
.Unauthorized("Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.");
|
||||
|
||||
throw GracefulException.Unauthorized("Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.");
|
||||
if (token.Active)
|
||||
throw GracefulException
|
||||
.BadRequest("The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.");
|
||||
throw GracefulException.BadRequest("The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.");
|
||||
// @formatter:on
|
||||
|
||||
if (MastodonOauthHelpers.ExpandScopes(request.Scopes ?? [])
|
||||
.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes))
|
||||
.Any())
|
||||
var invalidScope = MastodonOauthHelpers.ExpandScopes(request.Scopes ?? [])
|
||||
.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes))
|
||||
.Any();
|
||||
if (invalidScope)
|
||||
throw GracefulException.BadRequest("The requested scope is invalid, unknown, or malformed.");
|
||||
|
||||
token.Scopes = request.Scopes ?? token.Scopes;
|
||||
token.Active = true;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = new AuthSchemas.OauthTokenResponse
|
||||
return new OauthTokenResponse
|
||||
{
|
||||
CreatedAt = token.CreatedAt,
|
||||
Scopes = token.Scopes,
|
||||
AccessToken = token.Token
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("/oauth/revoke")]
|
||||
[ConsumesHybrid]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> RevokeOauthToken([FromHybrid] AuthSchemas.OauthTokenRevocationRequest request)
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
|
||||
public async Task<object> RevokeOauthToken([FromHybrid] OauthTokenRevocationRequest request)
|
||||
{
|
||||
var token = await db.OauthTokens.FirstOrDefaultAsync(p => p.Token == request.Token &&
|
||||
p.App.ClientId == request.ClientId &&
|
||||
p.App.ClientSecret == request.ClientSecret);
|
||||
if (token == null)
|
||||
throw GracefulException.Forbidden("You are not authorized to revoke this token");
|
||||
p.App.ClientSecret == request.ClientSecret) ??
|
||||
throw GracefulException.Forbidden("You are not authorized to revoke this token");
|
||||
|
||||
db.Remove(token);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
}
|
|
@ -31,8 +31,8 @@ public class ConversationsController(
|
|||
[HttpGet]
|
||||
[Authorize("read:statuses")]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ConversationEntity>))]
|
||||
public async Task<IActionResult> GetConversations(MastodonPaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<ConversationEntity>> GetConversations(MastodonPaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
|
@ -69,7 +69,7 @@ public class ConversationsController(
|
|||
|
||||
var notes = await noteRenderer.RenderManyAsync(conversations.Select(p => p.LastNote), user, accounts: accounts);
|
||||
|
||||
var res = conversations.Select(p => new ConversationEntity
|
||||
return conversations.Select(p => new ConversationEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Unread = p.Unread,
|
||||
|
@ -78,21 +78,19 @@ public class ConversationsController(
|
|||
.DefaultIfEmpty(accounts.First(a => a.Id == user.Id))
|
||||
.ToList()
|
||||
});
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize("write:conversations")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesErrors(HttpStatusCode.NotImplemented)]
|
||||
public IActionResult RemoveConversation(string id) => throw new GracefulException(HttpStatusCode.NotImplemented,
|
||||
"Iceshrimp.NET does not support this endpoint due to database schema differences to Mastodon");
|
||||
|
||||
[HttpPost("{id}/read")]
|
||||
[Authorize("write:conversations")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ConversationEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> MarkRead(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ConversationEntity> MarkRead(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var conversation = await db.Conversations(user)
|
||||
|
@ -137,15 +135,13 @@ public class ConversationsController(
|
|||
|
||||
var noteRendererDto = new NoteRenderer.NoteRendererDto { Accounts = accounts };
|
||||
|
||||
var res = new ConversationEntity
|
||||
return new ConversationEntity
|
||||
{
|
||||
Id = conversation.Id,
|
||||
Unread = conversation.Unread,
|
||||
LastStatus = await noteRenderer.RenderAsync(conversation.LastNote, user, data: noteRendererDto),
|
||||
Accounts = accounts
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
private class Conversation
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -26,33 +28,32 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize("read:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<FilterEntity>))]
|
||||
public async Task<IActionResult> GetFilters()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<FilterEntity>> GetFilters()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filters = await db.Filters.Where(p => p.User == user).ToListAsync();
|
||||
var res = filters.Select(FilterRenderer.RenderOne);
|
||||
|
||||
return Ok(res);
|
||||
return filters.Select(FilterRenderer.RenderOne);
|
||||
}
|
||||
|
||||
[HttpGet("{id:long}")]
|
||||
[Authorize("read:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<FilterEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetFilter(long id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<FilterEntity> GetFilter(long id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filter = await db.Filters.Where(p => p.User == user && p.Id == id).FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(FilterRenderer.RenderOne(filter));
|
||||
return FilterRenderer.RenderOne(filter);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("write:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<FilterEntity>))]
|
||||
public async Task<IActionResult> CreateFilter([FromHybrid] FilterSchemas.CreateFilterRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
public async Task<FilterEntity> CreateFilter([FromHybrid] FilterSchemas.CreateFilterRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var action = request.Action switch
|
||||
|
@ -102,14 +103,14 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
await queueSvc.BackgroundTaskQueue.ScheduleAsync(data, expiry.Value);
|
||||
}
|
||||
|
||||
return Ok(FilterRenderer.RenderOne(filter));
|
||||
return FilterRenderer.RenderOne(filter);
|
||||
}
|
||||
|
||||
[HttpPut("{id:long}")]
|
||||
[Authorize("write:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<FilterEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UpdateFilter(long id, [FromHybrid] FilterSchemas.UpdateFilterRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<FilterEntity> UpdateFilter(long id, [FromHybrid] FilterSchemas.UpdateFilterRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filter = await db.Filters.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
||||
|
@ -166,14 +167,15 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
await queueSvc.BackgroundTaskQueue.ScheduleAsync(data, expiry.Value);
|
||||
}
|
||||
|
||||
return Ok(FilterRenderer.RenderOne(filter));
|
||||
return FilterRenderer.RenderOne(filter);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:long}")]
|
||||
[Authorize("write:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> DeleteFilter(long id)
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<object> DeleteFilter(long id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filter = await db.Filters.Where(p => p.User == user && p.Id == id).FirstOrDefaultAsync() ??
|
||||
|
@ -183,27 +185,27 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseFilterRemoved(this, filter);
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
|
||||
[HttpGet("{id:long}/keywords")]
|
||||
[Authorize("read:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<FilterKeyword>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetFilterKeywords(long id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<FilterKeyword>> GetFilterKeywords(long id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filter = await db.Filters.Where(p => p.User == user && p.Id == id).FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(filter.Keywords.Select((p, i) => new FilterKeyword(p, filter.Id, i)));
|
||||
return filter.Keywords.Select((p, i) => new FilterKeyword(p, filter.Id, i));
|
||||
}
|
||||
|
||||
[HttpPost("{id:long}/keywords")]
|
||||
[Authorize("write:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FilterKeyword))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> AddFilterKeyword(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<FilterKeyword> AddFilterKeyword(
|
||||
long id, [FromHybrid] FilterSchemas.FilterKeywordsAttributes request
|
||||
)
|
||||
{
|
||||
|
@ -218,14 +220,14 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseFilterUpdated(this, filter);
|
||||
|
||||
return Ok(new FilterKeyword(keyword, filter.Id, filter.Keywords.Count - 1));
|
||||
return new FilterKeyword(keyword, filter.Id, filter.Keywords.Count - 1);
|
||||
}
|
||||
|
||||
[HttpGet("keywords/{filterId:long}-{keywordId:int}")]
|
||||
[Authorize("read:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FilterKeyword))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetFilterKeyword(long filterId, int keywordId)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<FilterKeyword> GetFilterKeyword(long filterId, int keywordId)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filter = await db.Filters.Where(p => p.User == user && p.Id == filterId).FirstOrDefaultAsync() ??
|
||||
|
@ -234,14 +236,14 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
if (filter.Keywords.Count < keywordId)
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(new FilterKeyword(filter.Keywords[keywordId], filter.Id, keywordId));
|
||||
return new FilterKeyword(filter.Keywords[keywordId], filter.Id, keywordId);
|
||||
}
|
||||
|
||||
[HttpPut("keywords/{filterId:long}-{keywordId:int}")]
|
||||
[Authorize("write:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FilterKeyword))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UpdateFilterKeyword(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<FilterKeyword> UpdateFilterKeyword(
|
||||
long filterId, int keywordId, [FromHybrid] FilterSchemas.FilterKeywordsAttributes request
|
||||
)
|
||||
{
|
||||
|
@ -257,14 +259,15 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseFilterUpdated(this, filter);
|
||||
|
||||
return Ok(new FilterKeyword(filter.Keywords[keywordId], filter.Id, keywordId));
|
||||
return new FilterKeyword(filter.Keywords[keywordId], filter.Id, keywordId);
|
||||
}
|
||||
|
||||
[HttpDelete("keywords/{filterId:long}-{keywordId:int}")]
|
||||
[Authorize("write:filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> DeleteFilterKeyword(long filterId, int keywordId)
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<object> DeleteFilterKeyword(long filterId, int keywordId)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var filter = await db.Filters.Where(p => p.User == user && p.Id == filterId).FirstOrDefaultAsync() ??
|
||||
|
@ -278,7 +281,7 @@ public class FilterController(DatabaseContext db, QueueService queueSvc, EventSe
|
|||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseFilterUpdated(this, filter);
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
|
||||
//TODO: status filters (first: what are they even for?)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -21,8 +23,8 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
public class InstanceController(DatabaseContext db, MetaService meta) : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/v1/instance")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(InstanceInfoV1Response))]
|
||||
public async Task<IActionResult> GetInstanceInfoV1([FromServices] IOptionsSnapshot<Config> config)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<InstanceInfoV1Response> GetInstanceInfoV1([FromServices] IOptionsSnapshot<Config> config)
|
||||
{
|
||||
var userCount =
|
||||
await db.Users.LongCountAsync(p => p.IsLocalUser && !Constants.SystemUsers.Contains(p.UsernameLower));
|
||||
|
@ -32,17 +34,15 @@ public class InstanceController(DatabaseContext db, MetaService meta) : Controll
|
|||
var (instanceName, instanceDescription, adminContact) =
|
||||
await meta.GetMany(MetaEntity.InstanceName, MetaEntity.InstanceDescription, MetaEntity.AdminContactEmail);
|
||||
|
||||
var res = new InstanceInfoV1Response(config.Value, instanceName, instanceDescription, adminContact)
|
||||
return new InstanceInfoV1Response(config.Value, instanceName, instanceDescription, adminContact)
|
||||
{
|
||||
Stats = new InstanceStats(userCount, noteCount, instanceCount)
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v2/instance")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(InstanceInfoV2Response))]
|
||||
public async Task<IActionResult> GetInstanceInfoV2([FromServices] IOptionsSnapshot<Config> config)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<InstanceInfoV2Response> GetInstanceInfoV2([FromServices] IOptionsSnapshot<Config> config)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow - TimeSpan.FromDays(30);
|
||||
var activeMonth = await db.Users.LongCountAsync(p => p.IsLocalUser &&
|
||||
|
@ -52,46 +52,38 @@ public class InstanceController(DatabaseContext db, MetaService meta) : Controll
|
|||
var (instanceName, instanceDescription, adminContact) =
|
||||
await meta.GetMany(MetaEntity.InstanceName, MetaEntity.InstanceDescription, MetaEntity.AdminContactEmail);
|
||||
|
||||
var res = new InstanceInfoV2Response(config.Value, instanceName, instanceDescription, adminContact)
|
||||
return new InstanceInfoV2Response(config.Value, instanceName, instanceDescription, adminContact)
|
||||
{
|
||||
Usage = new InstanceUsage { Users = new InstanceUsersUsage { ActiveMonth = activeMonth } }
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/custom_emojis")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<EmojiEntity>))]
|
||||
public async Task<IActionResult> GetCustomEmojis()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<EmojiEntity>> GetCustomEmojis()
|
||||
{
|
||||
var res = await db.Emojis.Where(p => p.Host == null)
|
||||
.Select(p => new EmojiEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Shortcode = p.Name,
|
||||
Url = p.PublicUrl,
|
||||
StaticUrl = p.PublicUrl, //TODO
|
||||
VisibleInPicker = true,
|
||||
Category = p.Category
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(res);
|
||||
return await db.Emojis.Where(p => p.Host == null)
|
||||
.Select(p => new EmojiEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Shortcode = p.Name,
|
||||
Url = p.PublicUrl,
|
||||
StaticUrl = p.PublicUrl, //TODO
|
||||
VisibleInPicker = true,
|
||||
Category = p.Category
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/instance/translation_languages")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dictionary<string, IEnumerable<string>>))]
|
||||
public IActionResult GetTranslationLanguages()
|
||||
{
|
||||
return Ok(new Dictionary<string, IEnumerable<string>>());
|
||||
}
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public Dictionary<string, IEnumerable<string>> GetTranslationLanguages() => new();
|
||||
|
||||
[HttpGet("/api/v1/instance/extended_description")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(InstanceExtendedDescription))]
|
||||
public async Task<IActionResult> GetExtendedDescription()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<InstanceExtendedDescription> GetExtendedDescription()
|
||||
{
|
||||
var description = await meta.Get(MetaEntity.InstanceDescription);
|
||||
var res = new InstanceExtendedDescription(description);
|
||||
return Ok(res);
|
||||
return new InstanceExtendedDescription(description);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
|
@ -28,51 +29,47 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize("read:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ListEntity>))]
|
||||
public async Task<IActionResult> GetLists()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<ListEntity>> GetLists()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
var res = await db.UserLists
|
||||
.Where(p => p.User == user)
|
||||
.Select(p => new ListEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Name,
|
||||
Exclusive = p.HideFromHomeTl
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(res);
|
||||
return await db.UserLists
|
||||
.Where(p => p.User == user)
|
||||
.Select(p => new ListEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Name,
|
||||
Exclusive = p.HideFromHomeTl
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Authorize("read:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ListEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetList(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ListEntity> GetList(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
var res = await db.UserLists
|
||||
.Where(p => p.User == user && p.Id == id)
|
||||
.Select(p => new ListEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Name,
|
||||
Exclusive = p.HideFromHomeTl
|
||||
})
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(res);
|
||||
return await db.UserLists
|
||||
.Where(p => p.User == user && p.Id == id)
|
||||
.Select(p => new ListEntity
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Name,
|
||||
Exclusive = p.HideFromHomeTl
|
||||
})
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("write:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ListEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> CreateList([FromHybrid] ListSchemas.ListCreationRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.UnprocessableEntity)]
|
||||
public async Task<ListEntity> CreateList([FromHybrid] ListSchemas.ListCreationRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
throw GracefulException.UnprocessableEntity("Validation failed: Title can't be blank");
|
||||
|
@ -90,21 +87,19 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
await db.AddAsync(list);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = new ListEntity
|
||||
return new ListEntity
|
||||
{
|
||||
Id = list.Id,
|
||||
Title = list.Name,
|
||||
Exclusive = list.HideFromHomeTl
|
||||
};
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[Authorize("write:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ListEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UpdateList(string id, [FromHybrid] ListSchemas.ListUpdateRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound, HttpStatusCode.UnprocessableEntity)]
|
||||
public async Task<ListEntity> UpdateList(string id, [FromHybrid] ListSchemas.ListUpdateRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var list = await db.UserLists
|
||||
|
@ -121,20 +116,20 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
db.Update(list);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = new ListEntity
|
||||
return new ListEntity
|
||||
{
|
||||
Id = list.Id,
|
||||
Title = list.Name,
|
||||
Exclusive = list.HideFromHomeTl
|
||||
};
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize("write:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> DeleteList(string id)
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<object> DeleteList(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var list = await db.UserLists
|
||||
|
@ -145,15 +140,15 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
db.Remove(list);
|
||||
await db.SaveChangesAsync();
|
||||
eventSvc.RaiseListMembersUpdated(this, list);
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
|
||||
[LinkPagination(40, 80)]
|
||||
[HttpGet("{id}/accounts")]
|
||||
[Authorize("read:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetListMembers(string id, MastodonPaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<List<AccountEntity>> GetListMembers(string id, MastodonPaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var list = await db.UserLists
|
||||
|
@ -161,7 +156,7 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = pq.Limit == 0
|
||||
return pq.Limit == 0
|
||||
? await db.UserListMembers
|
||||
.Where(p => p.UserList == list)
|
||||
.Include(p => p.User.UserProfile)
|
||||
|
@ -173,17 +168,15 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
.Include(p => p.User.UserProfile)
|
||||
.Select(p => p.User)
|
||||
.RenderAllForMastodonAsync(userRenderer);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/accounts")]
|
||||
[Authorize("write:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(MastodonErrorResponse))]
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound, HttpStatusCode.UnprocessableEntity)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||
public async Task<IActionResult> AddListMember(string id, [FromHybrid] ListSchemas.ListUpdateMembersRequest request)
|
||||
public async Task<object> AddListMember(string id, [FromHybrid] ListSchemas.ListUpdateMembersRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var list = await db.UserLists
|
||||
|
@ -214,14 +207,15 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
|
||||
eventSvc.RaiseListMembersUpdated(this, list);
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/accounts")]
|
||||
[Authorize("write:lists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> RemoveListMember(
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<object> RemoveListMember(
|
||||
string id, [FromHybrid] ListSchemas.ListUpdateMembersRequest request
|
||||
)
|
||||
{
|
||||
|
@ -237,6 +231,6 @@ public class ListController(DatabaseContext db, UserRenderer userRenderer, Event
|
|||
|
||||
eventSvc.RaiseListMembersUpdated(this, list);
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.Net.Mime;
|
|||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -24,28 +25,29 @@ public class MarkerController(DatabaseContext db) : ControllerBase
|
|||
{
|
||||
[HttpGet]
|
||||
[Authorize("read:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dictionary<string, MarkerEntity>))]
|
||||
public async Task<IActionResult> GetMarkers([FromQuery(Name = "timeline")] List<string> types)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<Dictionary<string, MarkerEntity>> GetMarkers([FromQuery(Name = "timeline")] List<string> types)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var markers = await db.Markers.Where(p => p.User == user && types.Select(DecodeType).Contains(p.Type))
|
||||
.ToListAsync();
|
||||
|
||||
var res = markers.ToDictionary(p => EncodeType(p.Type),
|
||||
p => new MarkerEntity
|
||||
{
|
||||
Position = p.Position,
|
||||
Version = p.Version,
|
||||
UpdatedAt = p.LastUpdatedAt.ToStringIso8601Like()
|
||||
});
|
||||
|
||||
return Ok(res);
|
||||
return markers.ToDictionary(p => EncodeType(p.Type),
|
||||
p => new MarkerEntity
|
||||
{
|
||||
Position = p.Position,
|
||||
Version = p.Version,
|
||||
UpdatedAt = p.LastUpdatedAt.ToStringIso8601Like()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("write:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dictionary<string, MarkerEntity>))]
|
||||
public async Task<IActionResult> SetMarkers([FromHybrid] Dictionary<string, MarkerSchemas.MarkerPosition> request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Conflict)]
|
||||
public async Task<Dictionary<string, MarkerEntity>> SetMarkers(
|
||||
[FromHybrid] Dictionary<string, MarkerSchemas.MarkerPosition> request
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
try
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -21,13 +23,12 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
[EnableCors("mastodon")]
|
||||
[EnableRateLimiting("sliding")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AttachmentEntity))]
|
||||
public class MediaController(DriveService driveSvc, DatabaseContext db) : ControllerBase
|
||||
{
|
||||
[HttpPost("/api/v1/media")]
|
||||
[HttpPost("/api/v2/media")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AttachmentEntity))]
|
||||
public async Task<IActionResult> UploadAttachment(MediaSchemas.UploadMediaRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<AttachmentEntity> UploadAttachment(MediaSchemas.UploadMediaRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var rq = new DriveFileCreationRequest
|
||||
|
@ -38,15 +39,15 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
|
|||
MimeType = request.File.ContentType
|
||||
};
|
||||
var file = await driveSvc.StoreFile(request.File.OpenReadStream(), user, rq);
|
||||
var res = RenderAttachment(file);
|
||||
|
||||
return Ok(res);
|
||||
return RenderAttachment(file);
|
||||
}
|
||||
|
||||
[HttpPut("/api/v1/media/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AttachmentEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UpdateAttachment(string id, [FromHybrid] MediaSchemas.UpdateMediaRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<AttachmentEntity> UpdateAttachment(
|
||||
string id, [FromHybrid] MediaSchemas.UpdateMediaRequest request
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
||||
|
@ -54,26 +55,24 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
|
|||
file.Comment = request.Description;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = RenderAttachment(file);
|
||||
return Ok(res);
|
||||
return RenderAttachment(file);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/media/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AttachmentEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetAttachment(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<AttachmentEntity> GetAttachment(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = RenderAttachment(file);
|
||||
return Ok(res);
|
||||
return RenderAttachment(file);
|
||||
}
|
||||
|
||||
[HttpPut("/api/v2/media/{id}")]
|
||||
[HttpGet("/api/v2/media/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public IActionResult FallbackMediaRoute([SuppressMessage("ReSharper", "UnusedParameter.Global")] string id) =>
|
||||
throw GracefulException.NotFound("This endpoint is not implemented, but some clients expect a 404 here.");
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
|
@ -26,41 +27,39 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
[HttpGet]
|
||||
[Authorize("read:notifications")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NotificationEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNotifications(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<List<NotificationEntity>> GetNotifications(
|
||||
MastodonPaginationQuery query, NotificationSchemas.GetNotificationsRequest request
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var res = await db.Notifications
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Notifiee == user)
|
||||
.Where(p => p.Notifier != null)
|
||||
.Where(p => p.Type == NotificationType.Follow ||
|
||||
p.Type == NotificationType.Mention ||
|
||||
p.Type == NotificationType.Reply ||
|
||||
p.Type == NotificationType.Renote ||
|
||||
p.Type == NotificationType.Quote ||
|
||||
p.Type == NotificationType.Like ||
|
||||
p.Type == NotificationType.PollEnded ||
|
||||
p.Type == NotificationType.FollowRequestReceived ||
|
||||
p.Type == NotificationType.Edit)
|
||||
.FilterByGetNotificationsRequest(request)
|
||||
.EnsureNoteVisibilityFor(p => p.Note, user)
|
||||
.FilterHiddenNotifications(user, db)
|
||||
.Paginate(p => p.MastoId, query, ControllerContext)
|
||||
.PrecomputeNoteVisibilities(user)
|
||||
.RenderAllForMastodonAsync(notificationRenderer, user);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notifications
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Notifiee == user)
|
||||
.Where(p => p.Notifier != null)
|
||||
.Where(p => p.Type == NotificationType.Follow ||
|
||||
p.Type == NotificationType.Mention ||
|
||||
p.Type == NotificationType.Reply ||
|
||||
p.Type == NotificationType.Renote ||
|
||||
p.Type == NotificationType.Quote ||
|
||||
p.Type == NotificationType.Like ||
|
||||
p.Type == NotificationType.PollEnded ||
|
||||
p.Type == NotificationType.FollowRequestReceived ||
|
||||
p.Type == NotificationType.Edit)
|
||||
.FilterByGetNotificationsRequest(request)
|
||||
.EnsureNoteVisibilityFor(p => p.Note, user)
|
||||
.FilterHiddenNotifications(user, db)
|
||||
.Paginate(p => p.MastoId, query, ControllerContext)
|
||||
.PrecomputeNoteVisibilities(user)
|
||||
.RenderAllForMastodonAsync(notificationRenderer, user);
|
||||
}
|
||||
|
||||
[HttpGet("{id:long}")]
|
||||
[Authorize("read:notifications")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NotificationEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNotification(long id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<NotificationEntity> GetNotification(long id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notification = await db.Notifications
|
||||
|
@ -72,7 +71,6 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = await notificationRenderer.RenderAsync(notification.EnforceRenoteReplyVisibility(p => p.Note), user);
|
||||
|
||||
return Ok(res);
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
|
@ -32,9 +34,9 @@ public class PollController(
|
|||
) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PollEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetPoll(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<PollEntity> GetPoll(string id)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -44,16 +46,14 @@ public class PollController(
|
|||
throw GracefulException.RecordNotFound();
|
||||
var poll = await db.Polls.Where(p => p.Note == note).FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
var res = await pollRenderer.RenderAsync(poll, user);
|
||||
return Ok(res);
|
||||
return await pollRenderer.RenderAsync(poll, user);
|
||||
}
|
||||
|
||||
[HttpPost("votes")]
|
||||
[Authorize("read:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PollEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> VotePoll(string id, [FromHybrid] PollSchemas.PollVoteRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||
public async Task<PollEntity> VotePoll(string id, [FromHybrid] PollSchemas.PollVoteRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -118,7 +118,6 @@ public class PollController(
|
|||
await pollSvc.RegisterPollVote(vote, poll, note, votes.IndexOf(vote) == 0);
|
||||
|
||||
await db.ReloadEntityAsync(poll);
|
||||
var res = await pollRenderer.RenderAsync(poll, user);
|
||||
return Ok(res);
|
||||
return await pollRenderer.RenderAsync(poll, user);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
|
@ -11,6 +12,8 @@ using Microsoft.AspNetCore.Cors;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static Iceshrimp.Backend.Controllers.Mastodon.Schemas.PushSchemas;
|
||||
using PushSubscription = Iceshrimp.Backend.Core.Database.Tables.PushSubscription;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||
|
||||
|
@ -24,11 +27,13 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
public class PushController(DatabaseContext db, MetaService meta) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PushSchemas.PushSubscription))]
|
||||
public async Task<IActionResult> RegisterSubscription([FromHybrid] PushSchemas.RegisterPushRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Unauthorized)]
|
||||
public async Task<PushSchemas.PushSubscription> RegisterSubscription(
|
||||
[FromHybrid] RegisterPushRequest request
|
||||
)
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var token = HttpContext.GetOauthToken() ?? throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var pushSubscription = await db.PushSubscriptions.FirstOrDefaultAsync(p => p.OauthToken == token);
|
||||
if (pushSubscription == null)
|
||||
{
|
||||
|
@ -49,54 +54,46 @@ public class PushController(DatabaseContext db, MetaService meta) : ControllerBa
|
|||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var res = await RenderSubscription(pushSubscription);
|
||||
|
||||
return Ok(res);
|
||||
return await RenderSubscription(pushSubscription);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PushSchemas.PushSubscription))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> EditSubscription([FromHybrid] PushSchemas.EditPushRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Unauthorized, HttpStatusCode.NotFound)]
|
||||
public async Task<PushSchemas.PushSubscription> EditSubscription([FromHybrid] EditPushRequest request)
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var token = HttpContext.GetOauthToken() ?? throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var pushSubscription = await db.PushSubscriptions.FirstOrDefaultAsync(p => p.OauthToken == token) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
throw GracefulException.NotFound("Push subscription not found");
|
||||
|
||||
pushSubscription.Types = GetTypes(request.Data.Alerts);
|
||||
pushSubscription.Policy = GetPolicy(request.Data.Policy);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = await RenderSubscription(pushSubscription);
|
||||
|
||||
return Ok(res);
|
||||
return await RenderSubscription(pushSubscription);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PushSchemas.PushSubscription))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetSubscription()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Unauthorized, HttpStatusCode.NotFound)]
|
||||
public async Task<PushSchemas.PushSubscription> GetSubscription()
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var token = HttpContext.GetOauthToken() ?? throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var pushSubscription = await db.PushSubscriptions.FirstOrDefaultAsync(p => p.OauthToken == token) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
throw GracefulException.NotFound("Push subscription not found");
|
||||
|
||||
var res = await RenderSubscription(pushSubscription);
|
||||
|
||||
return Ok(res);
|
||||
return await RenderSubscription(pushSubscription);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
public async Task<IActionResult> DeleteSubscription()
|
||||
[OverrideResultType<object>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Unauthorized)]
|
||||
public async Task<object> DeleteSubscription()
|
||||
{
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
var token = HttpContext.GetOauthToken() ?? throw GracefulException.Unauthorized("The access token is invalid");
|
||||
await db.PushSubscriptions.Where(p => p.OauthToken == token).ExecuteDeleteAsync();
|
||||
|
||||
return Ok(new object());
|
||||
return new object();
|
||||
}
|
||||
|
||||
private static PushSubscription.PushPolicy GetPolicy(string policy)
|
||||
|
@ -123,7 +120,7 @@ public class PushController(DatabaseContext db, MetaService meta) : ControllerBa
|
|||
};
|
||||
}
|
||||
|
||||
private static List<string> GetTypes(PushSchemas.Alerts alerts)
|
||||
private static List<string> GetTypes(Alerts alerts)
|
||||
{
|
||||
List<string> types = [];
|
||||
|
||||
|
@ -155,7 +152,7 @@ public class PushController(DatabaseContext db, MetaService meta) : ControllerBa
|
|||
Endpoint = sub.Endpoint,
|
||||
ServerKey = await meta.Get(MetaEntity.VapidPublicKey),
|
||||
Policy = GetPolicyString(sub.Policy),
|
||||
Alerts = new PushSchemas.Alerts
|
||||
Alerts = new Alerts
|
||||
{
|
||||
Favourite = sub.Types.Contains("favourite"),
|
||||
Follow = sub.Types.Contains("follow"),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using System.Text.RegularExpressions;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
|
@ -37,38 +38,36 @@ public class SearchController(
|
|||
[HttpGet("/api/v2/search")]
|
||||
[Authorize("read:search")]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(SearchSchemas.SearchResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> Search(SearchSchemas.SearchRequest search, MastodonPaginationQuery pagination)
|
||||
{
|
||||
if (search.Query == null)
|
||||
throw GracefulException.BadRequest("Query is missing or invalid");
|
||||
|
||||
var result = new SearchSchemas.SearchResponse
|
||||
{
|
||||
Accounts = search.Type is null or "accounts" ? await SearchUsersAsync(search, pagination) : [],
|
||||
Statuses = search.Type is null or "statuses" ? await SearchNotesAsync(search, pagination) : [],
|
||||
Hashtags = search.Type is null or "hashtags" ? await SearchTagsAsync(search, pagination) : []
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/accounts/search")]
|
||||
[Authorize("read:accounts")]
|
||||
[LinkPagination(20, 40, true)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> SearchAccounts(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<SearchSchemas.SearchResponse> Search(
|
||||
SearchSchemas.SearchRequest search, MastodonPaginationQuery pagination
|
||||
)
|
||||
{
|
||||
if (search.Query == null)
|
||||
throw GracefulException.BadRequest("Query is missing or invalid");
|
||||
|
||||
var result = await SearchUsersAsync(search, pagination);
|
||||
return new SearchSchemas.SearchResponse
|
||||
{
|
||||
Accounts = search.Type is null or "accounts" ? await SearchUsersAsync(search, pagination) : [],
|
||||
Statuses = search.Type is null or "statuses" ? await SearchNotesAsync(search, pagination) : [],
|
||||
Hashtags = search.Type is null or "hashtags" ? await SearchTagsAsync(search, pagination) : []
|
||||
};
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
[HttpGet("/api/v1/accounts/search")]
|
||||
[Authorize("read:accounts")]
|
||||
[LinkPagination(20, 40, true)]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<List<AccountEntity>> SearchAccounts(
|
||||
SearchSchemas.SearchRequest search, MastodonPaginationQuery pagination
|
||||
)
|
||||
{
|
||||
if (search.Query == null)
|
||||
throw GracefulException.BadRequest("Query is missing or invalid");
|
||||
|
||||
return await SearchUsersAsync(search, pagination);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using AsyncKeyedLock;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
|
@ -47,9 +48,9 @@ public class StatusController(
|
|||
|
||||
[HttpGet("{id}")]
|
||||
[Authenticate("read:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> GetNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -68,15 +69,14 @@ public class StatusController(
|
|||
if (security.Value.PublicPreview <= Enums.PublicPreview.Restricted && note.User.IsRemoteUser && user == null)
|
||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||
|
||||
var res = await noteRenderer.RenderAsync(note.EnforceRenoteReplyVisibility(), user);
|
||||
return Ok(res);
|
||||
return await noteRenderer.RenderAsync(note.EnforceRenoteReplyVisibility(), user);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/context")]
|
||||
[Authenticate("read:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusContext))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetStatusContext(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<StatusContext> GetStatusContext(string id)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -102,7 +102,7 @@ public class StatusController(
|
|||
.AnyAsync();
|
||||
|
||||
if (!shouldShowContext)
|
||||
return Ok(new StatusContext { Ancestors = [], Descendants = [] });
|
||||
return new StatusContext { Ancestors = [], Descendants = [] };
|
||||
|
||||
var ancestors = await db.NoteAncestors(id, maxAncestors)
|
||||
.IncludeCommonProperties()
|
||||
|
@ -119,19 +119,17 @@ public class StatusController(
|
|||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Threads);
|
||||
|
||||
var res = new StatusContext
|
||||
return new StatusContext
|
||||
{
|
||||
Ancestors = ancestors.OrderAncestors(), Descendants = descendants.OrderDescendants()
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/favourite")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> LikeNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> LikeNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -150,9 +148,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/unfavourite")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnlikeNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> UnlikeNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -171,9 +169,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/react/{reaction}")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> ReactNote(string id, string reaction)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> ReactNote(string id, string reaction)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -192,9 +190,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/unreact/{reaction}")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnreactNote(string id, string reaction)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> UnreactNote(string id, string reaction)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -213,9 +211,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/bookmark")]
|
||||
[Authorize("write:bookmarks")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> BookmarkNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> BookmarkNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -231,9 +229,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/unbookmark")]
|
||||
[Authorize("write:bookmarks")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnbookmarkNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> UnbookmarkNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -249,10 +247,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/pin")]
|
||||
[Authorize("write:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> PinNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound, HttpStatusCode.UnprocessableEntity)]
|
||||
public async Task<StatusEntity> PinNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -267,10 +264,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/unpin")]
|
||||
[Authorize("write:accounts")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UnpinNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> UnpinNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id).FirstOrDefaultAsync() ??
|
||||
|
@ -282,9 +278,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/reblog")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> Renote(string id, [FromHybrid] StatusSchemas.ReblogRequest? request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> Renote(string id, [FromHybrid] StatusSchemas.ReblogRequest? request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var renote = await db.Notes.IncludeCommonProperties()
|
||||
|
@ -312,9 +308,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost("{id}/unreblog")]
|
||||
[Authorize("write:favourites")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> UndoRenote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> UndoRenote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -330,9 +326,9 @@ public class StatusController(
|
|||
|
||||
[HttpPost]
|
||||
[Authorize("write:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> PostNote([FromHybrid] StatusSchemas.PostStatusRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.UnprocessableEntity)]
|
||||
public async Task<StatusEntity> PostNote([FromHybrid] StatusSchemas.PostStatusRequest request)
|
||||
{
|
||||
var token = HttpContext.GetOauthToken() ?? throw new Exception("Token must not be null at this stage");
|
||||
var user = token.User;
|
||||
|
@ -454,17 +450,14 @@ public class StatusController(
|
|||
if (idempotencyKey != null)
|
||||
await cache.SetAsync($"idempotency:{user.Id}:{idempotencyKey}", note.Id, TimeSpan.FromHours(24));
|
||||
|
||||
var res = await noteRenderer.RenderAsync(note, user);
|
||||
|
||||
return Ok(res);
|
||||
return await noteRenderer.RenderAsync(note, user);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[Authorize("write:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> EditNote(string id, [FromHybrid] StatusSchemas.EditStatusRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> EditNote(string id, [FromHybrid] StatusSchemas.EditStatusRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes
|
||||
|
@ -502,16 +495,14 @@ public class StatusController(
|
|||
}
|
||||
|
||||
note = await noteSvc.UpdateNoteAsync(note, request.Text, request.Cw, attachments, poll);
|
||||
var res = await noteRenderer.RenderAsync(note, user);
|
||||
|
||||
return Ok(res);
|
||||
return await noteRenderer.RenderAsync(note, user);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize("write:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> DeleteNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusEntity> DeleteNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
||||
|
@ -519,36 +510,33 @@ public class StatusController(
|
|||
|
||||
var res = await noteRenderer.RenderAsync(note, user, data: new NoteRenderer.NoteRendererDto { Source = true });
|
||||
await noteSvc.DeleteNoteAsync(note);
|
||||
|
||||
return Ok(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/source")]
|
||||
[Authorize("read:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusSource))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteSource(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<StatusSource> GetNoteSource(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var res = await db.Notes.Where(p => p.Id == id && p.User == user)
|
||||
.Select(p => new StatusSource
|
||||
{
|
||||
Id = p.Id,
|
||||
ContentWarning = p.Cw ?? "",
|
||||
Text = p.Text ?? ""
|
||||
})
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notes.Where(p => p.Id == id && p.User == user)
|
||||
.Select(p => new StatusSource
|
||||
{
|
||||
Id = p.Id,
|
||||
ContentWarning = p.Cw ?? "",
|
||||
Text = p.Text ?? ""
|
||||
})
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/favourited_by")]
|
||||
[Authenticate("read:statuses")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteLikes(string id, MastodonPaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<AccountEntity>> GetNoteLikes(string id, MastodonPaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -570,16 +558,15 @@ public class StatusController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(likes);
|
||||
var res = await userRenderer.RenderManyAsync(likes.Select(p => p.Entity));
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderManyAsync(likes.Select(p => p.Entity));
|
||||
}
|
||||
|
||||
[HttpGet("{id}/reblogged_by")]
|
||||
[Authenticate("read:statuses")]
|
||||
[LinkPagination(40, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteRenotes(string id, MastodonPaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<AccountEntity>> GetNoteRenotes(string id, MastodonPaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -604,15 +591,14 @@ public class StatusController(
|
|||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(renotes);
|
||||
var res = await userRenderer.RenderManyAsync(renotes.Select(p => p.Entity));
|
||||
return Ok(res);
|
||||
return await userRenderer.RenderManyAsync(renotes.Select(p => p.Entity));
|
||||
}
|
||||
|
||||
[HttpGet("{id}/history")]
|
||||
[Authenticate("read:statuses")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<AccountEntity>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteEditHistory(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task<List<StatusEdit>> GetNoteEditHistory(string id)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
if (security.Value.PublicPreview == Enums.PublicPreview.Lockdown && user == null)
|
||||
|
@ -629,7 +615,6 @@ public class StatusController(
|
|||
if (security.Value.PublicPreview <= Enums.PublicPreview.Restricted && note.User.IsRemoteUser && user == null)
|
||||
throw GracefulException.Forbidden("Public preview is disabled on this instance");
|
||||
|
||||
var res = await noteRenderer.RenderHistoryAsync(note);
|
||||
return Ok(res);
|
||||
return await noteRenderer.RenderHistoryAsync(note);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
|
@ -27,84 +28,73 @@ public class TimelineController(DatabaseContext db, NoteRenderer noteRenderer, C
|
|||
{
|
||||
[Authorize("read:statuses")]
|
||||
[HttpGet("home")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
public async Task<IActionResult> GetHomeTimeline(MastodonPaginationQuery query)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<StatusEntity>> GetHomeTimeline(MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var heuristic = await QueryableTimelineExtensions.GetHeuristic(user, db, cache);
|
||||
|
||||
var res = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.FilterByFollowingAndOwn(user, db, heuristic)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db, filterHiddenListMembers: true)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Home);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.FilterByFollowingAndOwn(user, db, heuristic)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db, filterHiddenListMembers: true)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Home);
|
||||
}
|
||||
|
||||
[Authorize("read:statuses")]
|
||||
[HttpGet("public")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
public async Task<IActionResult> GetPublicTimeline(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<StatusEntity>> GetPublicTimeline(
|
||||
MastodonPaginationQuery query, TimelineSchemas.PublicTimelineRequest request
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
var res = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.HasVisibility(Note.NoteVisibility.Public)
|
||||
.FilterByPublicTimelineRequest(request)
|
||||
.FilterHidden(user, db)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Public);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.HasVisibility(Note.NoteVisibility.Public)
|
||||
.FilterByPublicTimelineRequest(request)
|
||||
.FilterHidden(user, db)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Public);
|
||||
}
|
||||
|
||||
[Authorize("read:statuses")]
|
||||
[HttpGet("tag/{hashtag}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
public async Task<IActionResult> GetHashtagTimeline(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<StatusEntity>> GetHashtagTimeline(
|
||||
string hashtag, MastodonPaginationQuery query, TimelineSchemas.HashtagTimelineRequest request
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
var res = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Tags.Contains(hashtag.ToLowerInvariant()))
|
||||
.FilterByHashtagTimelineRequest(request)
|
||||
.FilterHidden(user, db)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Public);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Tags.Contains(hashtag.ToLowerInvariant()))
|
||||
.FilterByHashtagTimelineRequest(request)
|
||||
.FilterHidden(user, db)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Public);
|
||||
}
|
||||
|
||||
[Authorize("read:lists")]
|
||||
[HttpGet("list/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<StatusEntity>))]
|
||||
public async Task<IActionResult> GetListTimeline(string id, MastodonPaginationQuery query)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<StatusEntity>> GetListTimeline(string id, MastodonPaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (!await db.UserLists.AnyAsync(p => p.Id == id && p.User == user))
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var res = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => db.UserListMembers.Any(l => l.UserListId == id && l.UserId == p.UserId))
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Lists);
|
||||
|
||||
return Ok(res);
|
||||
return await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => db.UserListMembers.Any(l => l.UserListId == id && l.UserId == p.UserId))
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db)
|
||||
.Paginate(query, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer, user, Filter.FilterContext.Lists);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
|
||||
public class ConsumesActivityStreamsPayload() : ConsumesAttribute("application/activity+json",
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"");
|
|
@ -0,0 +1,30 @@
|
|||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class ProducesErrorsAttribute(HttpStatusCode statusCode, params HttpStatusCode[] additional)
|
||||
: ProducesResponseTypeAttribute((int)statusCode)
|
||||
{
|
||||
public IEnumerable<HttpStatusCode> StatusCodes => additional.Prepend(statusCode);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class ProducesResultsAttribute(HttpStatusCode statusCode, params HttpStatusCode[] additional)
|
||||
: ProducesResponseTypeAttribute((int)statusCode)
|
||||
{
|
||||
public IEnumerable<HttpStatusCode> StatusCodes => additional.Prepend(statusCode);
|
||||
}
|
||||
|
||||
public abstract class OverrideResultTypeAttribute(Type type) : Attribute
|
||||
{
|
||||
public Type Type => type;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class OverrideResultTypeAttribute<T>() : OverrideResultTypeAttribute(typeof(T));
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class ProducesActivityStreamsPayload() : ProducesAttribute("application/activity+json",
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"");
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Federation;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
|
@ -15,6 +16,7 @@ using Iceshrimp.Shared.Schemas.Web;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Web;
|
||||
|
||||
|
@ -28,13 +30,16 @@ public class AdminController(
|
|||
DatabaseContext db,
|
||||
ActivityPubController apController,
|
||||
ActivityPub.ActivityFetcherService fetchSvc,
|
||||
ActivityPub.NoteRenderer noteRenderer,
|
||||
ActivityPub.UserRenderer userRenderer,
|
||||
IOptions<Config.InstanceSection> config,
|
||||
QueueService queueSvc
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpPost("invites/generate")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(InviteResponse))]
|
||||
public async Task<IActionResult> GenerateInvite()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<InviteResponse> GenerateInvite()
|
||||
{
|
||||
var invite = new RegistrationInvite
|
||||
{
|
||||
|
@ -46,18 +51,15 @@ public class AdminController(
|
|||
await db.AddAsync(invite);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = new InviteResponse { Code = invite.Code };
|
||||
|
||||
return Ok(res);
|
||||
return new InviteResponse { Code = invite.Code };
|
||||
}
|
||||
|
||||
[HttpPost("users/{id}/reset-password")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ResetPassword(string id, [FromBody] ResetPasswordRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||
public async Task ResetPassword(string id, [FromBody] ResetPasswordRequest request)
|
||||
{
|
||||
var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == id && p.UserHost == null) ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
@ -67,16 +69,12 @@ public class AdminController(
|
|||
|
||||
profile.Password = AuthHelpers.HashPassword(request.Password);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("instances/{host}/force-state/{state}")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ForceInstanceState(string host, AdminSchemas.InstanceState state)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task ForceInstanceState(string host, AdminSchemas.InstanceState state)
|
||||
{
|
||||
var instance = await db.Instances.FirstOrDefaultAsync(p => p.Host == host.ToLowerInvariant()) ??
|
||||
throw GracefulException.NotFound("Instance not found");
|
||||
|
@ -97,93 +95,97 @@ public class AdminController(
|
|||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("queue/jobs/{id::guid}/retry")]
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RetryQueueJob(Guid id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||
public async Task RetryQueueJob(Guid id)
|
||||
{
|
||||
var job = await db.Jobs.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.NotFound($"Job {id} was not found.");
|
||||
|
||||
await queueSvc.RetryJobAsync(job);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/notes/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASNote))]
|
||||
[OverrideResultType<ASNote>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
public async Task<IActionResult> GetNoteActivity(string id, [FromServices] ActivityPub.NoteRenderer noteRenderer)
|
||||
public async Task<JObject> GetNoteActivity(string id)
|
||||
{
|
||||
var note = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserHost == null);
|
||||
if (note == null) return NotFound();
|
||||
var rendered = await noteRenderer.RenderAsync(note);
|
||||
var compacted = rendered.Compact();
|
||||
return Ok(compacted);
|
||||
if (note == null) throw GracefulException.NotFound("Note not found");
|
||||
var rendered = await noteRenderer.RenderAsync(note);
|
||||
return rendered.Compact() ?? throw new Exception("Failed to compact JSON-LD payload");
|
||||
}
|
||||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/notes/{id}/activity")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASAnnounce))]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
[OverrideResultType<ASAnnounce>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
[ProducesActivityStreamsPayload]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage")]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery")]
|
||||
public async Task<IActionResult> GetRenoteActivity(
|
||||
string id, [FromServices] ActivityPub.NoteRenderer noteRenderer,
|
||||
[FromServices] ActivityPub.UserRenderer userRenderer, [FromServices] IOptions<Config.InstanceSection> config
|
||||
)
|
||||
public async Task<JObject> GetRenoteActivity(string id)
|
||||
{
|
||||
var note = await db.Notes
|
||||
.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Id == id && p.UserHost == null);
|
||||
if (note is not { IsPureRenote: true, Renote: not null }) return NotFound();
|
||||
var rendered = ActivityPub.ActivityRenderer.RenderAnnounce(noteRenderer.RenderLite(note.Renote),
|
||||
note.GetPublicUri(config.Value),
|
||||
userRenderer.RenderLite(note.User),
|
||||
note.Visibility,
|
||||
note.User.GetPublicUri(config.Value) + "/followers");
|
||||
var compacted = rendered.Compact();
|
||||
return Ok(compacted);
|
||||
.Where(p => p.Id == id && p.UserHost == null && p.IsPureRenote && p.Renote != null)
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.NotFound("Note not found");
|
||||
|
||||
return ActivityPub.ActivityRenderer
|
||||
.RenderAnnounce(noteRenderer.RenderLite(note.Renote!),
|
||||
note.GetPublicUri(config.Value),
|
||||
userRenderer.RenderLite(note.User),
|
||||
note.Visibility,
|
||||
note.User.GetPublicUri(config.Value) + "/followers")
|
||||
.Compact();
|
||||
}
|
||||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/users/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActor))]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
public async Task<IActionResult> GetUserActivity(string id)
|
||||
[OverrideResultType<ASActor>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
[ProducesActivityStreamsPayload]
|
||||
public async Task<ActionResult<JObject>> GetUserActivity(string id)
|
||||
{
|
||||
return await apController.GetUser(id);
|
||||
}
|
||||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/users/{id}/collections/featured")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActor))]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
public async Task<IActionResult> GetUserFeaturedActivity(string id)
|
||||
[OverrideResultType<ASOrderedCollection>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesActivityStreamsPayload]
|
||||
public async Task<JObject> GetUserFeaturedActivity(string id)
|
||||
{
|
||||
return await apController.GetUserFeatured(id);
|
||||
}
|
||||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/users/@{acct}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASActor))]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
public async Task<IActionResult> GetUserActivityByUsername(string acct)
|
||||
[OverrideResultType<ASActor>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesActivityStreamsPayload]
|
||||
public async Task<ActionResult<JObject>> GetUserActivityByUsername(string acct)
|
||||
{
|
||||
return await apController.GetUserByUsername(acct);
|
||||
}
|
||||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/fetch")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASObject))]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
[OverrideResultType<ASObject>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesActivityStreamsPayload]
|
||||
public async Task<IActionResult> FetchActivityAsync([FromQuery] string uri, [FromQuery] string? userId)
|
||||
{
|
||||
var user = userId != null ? await db.Users.FirstOrDefaultAsync(p => p.Id == userId && p.IsLocalUser) : null;
|
||||
|
@ -194,9 +196,10 @@ public class AdminController(
|
|||
|
||||
[UseNewtonsoftJson]
|
||||
[HttpGet("activities/fetch-raw")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ASObject))]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ErrorResponse))]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
[OverrideResultType<ASObject>]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.UnprocessableEntity)]
|
||||
[ProducesActivityStreamsPayload]
|
||||
public async Task FetchRawActivityAsync([FromQuery] string uri, [FromQuery] string? userId)
|
||||
{
|
||||
var user = userId != null ? await db.Users.FirstOrDefaultAsync(p => p.Id == userId && p.IsLocalUser) : null;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Web.Renderers;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
|
@ -22,34 +24,31 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere
|
|||
{
|
||||
[HttpGet]
|
||||
[Authenticate]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
public async Task<IActionResult> GetAuthStatus()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<AuthResponse> GetAuthStatus()
|
||||
{
|
||||
var session = HttpContext.GetSession();
|
||||
if (session == null) return new AuthResponse { Status = AuthStatusEnum.Guest };
|
||||
|
||||
if (session == null)
|
||||
return Ok(new AuthResponse { Status = AuthStatusEnum.Guest });
|
||||
|
||||
return Ok(new AuthResponse
|
||||
return new AuthResponse
|
||||
{
|
||||
Status = session.Active ? AuthStatusEnum.Authenticated : AuthStatusEnum.TwoFactor,
|
||||
Token = session.Token,
|
||||
IsAdmin = session.User.IsAdmin,
|
||||
IsModerator = session.User.IsModerator,
|
||||
User = await userRenderer.RenderOne(session.User)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[HideRequestDuration]
|
||||
[EnableRateLimiting("auth")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden)]
|
||||
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action",
|
||||
Justification = "Argon2 is execution time-heavy by design")]
|
||||
public async Task<IActionResult> Login([FromBody] AuthRequest request)
|
||||
public async Task<AuthResponse> Login([FromBody] AuthRequest request)
|
||||
{
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.IsLocalUser &&
|
||||
p.UsernameLower == request.Username.ToLowerInvariant());
|
||||
|
@ -76,24 +75,22 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere
|
|||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Ok(new AuthResponse
|
||||
return new AuthResponse
|
||||
{
|
||||
Status = session.Active ? AuthStatusEnum.Authenticated : AuthStatusEnum.TwoFactor,
|
||||
Token = session.Token,
|
||||
IsAdmin = session.User.IsAdmin,
|
||||
IsModerator = session.User.IsModerator,
|
||||
User = await userRenderer.RenderOne(user)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[EnableRateLimiting("auth")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> Register([FromBody] RegistrationRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden)]
|
||||
public async Task<AuthResponse> Register([FromBody] RegistrationRequest request)
|
||||
{
|
||||
//TODO: captcha support
|
||||
|
||||
|
@ -106,11 +103,11 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere
|
|||
[Authorize]
|
||||
[EnableRateLimiting("auth")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action",
|
||||
Justification = "Argon2 is execution time-heavy by design")]
|
||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
public async Task<AuthResponse> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUser() ?? throw new GracefulException("HttpContext.GetUser() was null");
|
||||
var userProfile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
|
@ -17,7 +18,6 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
|||
public class DriveController(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorage,
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
|
||||
IOptionsSnapshot<Config.StorageSection> options,
|
||||
ILogger<DriveController> logger,
|
||||
DriveService driveSvc
|
||||
|
@ -25,6 +25,8 @@ public class DriveController(
|
|||
{
|
||||
[EnableCors("drive")]
|
||||
[HttpGet("/files/{accessKey}")]
|
||||
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.NoContent)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> GetFileByAccessKey(string accessKey)
|
||||
{
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.AccessKey == accessKey ||
|
||||
|
@ -33,7 +35,7 @@ public class DriveController(
|
|||
if (file == null)
|
||||
{
|
||||
Response.Headers.CacheControl = "max-age=86400";
|
||||
return NotFound();
|
||||
throw GracefulException.NotFound("File not found");
|
||||
}
|
||||
|
||||
if (file.StoredInternal)
|
||||
|
@ -42,7 +44,7 @@ public class DriveController(
|
|||
if (string.IsNullOrWhiteSpace(pathBase))
|
||||
{
|
||||
logger.LogError("Failed to get file {accessKey} from local storage: path does not exist", accessKey);
|
||||
return NotFound();
|
||||
throw GracefulException.NotFound("File not found");
|
||||
}
|
||||
|
||||
var path = Path.Join(pathBase, accessKey);
|
||||
|
@ -64,7 +66,7 @@ public class DriveController(
|
|||
if (stream == null)
|
||||
{
|
||||
logger.LogError("Failed to get file {accessKey} from object storage", accessKey);
|
||||
return NotFound();
|
||||
throw GracefulException.NotFound("File not found");
|
||||
}
|
||||
|
||||
Response.Headers.CacheControl = "max-age=31536000, immutable";
|
||||
|
@ -76,8 +78,9 @@ public class DriveController(
|
|||
[HttpPost]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DriveFileResponse))]
|
||||
public async Task<IActionResult> UploadFile(IFormFile file)
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<DriveFileResponse> UploadFile(IFormFile file)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var request = new DriveFileCreationRequest
|
||||
|
@ -93,15 +96,16 @@ public class DriveController(
|
|||
[HttpGet("{id}")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DriveFileResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetFileById(string id)
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<DriveFileResponse> GetFileById(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id);
|
||||
if (file == null) return NotFound();
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
||||
throw GracefulException.NotFound("File not found");
|
||||
|
||||
var res = new DriveFileResponse
|
||||
return new DriveFileResponse
|
||||
{
|
||||
Id = file.Id,
|
||||
Url = file.PublicUrl,
|
||||
|
@ -111,21 +115,20 @@ public class DriveController(
|
|||
Description = file.Comment,
|
||||
Sensitive = file.IsSensitive
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DriveFileResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> UpdateFile(string id, UpdateDriveFileRequest request)
|
||||
[Produces(MediaTypeNames.Application.Json)]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<DriveFileResponse> UpdateFile(string id, UpdateDriveFileRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id);
|
||||
if (file == null) return NotFound();
|
||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
|
||||
throw GracefulException.NotFound("File not found");
|
||||
|
||||
file.Name = request.Filename ?? file.Name;
|
||||
file.IsSensitive = request.Sensitive ?? file.IsSensitive;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
|
@ -22,36 +24,33 @@ public class EmojiController(
|
|||
) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<EmojiResponse>))]
|
||||
public async Task<IActionResult> GetAllEmoji()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<EmojiResponse>> GetAllEmoji()
|
||||
{
|
||||
var res = await db.Emojis
|
||||
.Where(p => p.Host == null)
|
||||
.Select(p => new EmojiResponse
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Uri = p.Uri,
|
||||
Aliases = p.Aliases,
|
||||
Category = p.Category,
|
||||
PublicUrl = p.PublicUrl,
|
||||
License = p.License
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(res);
|
||||
return await db.Emojis
|
||||
.Where(p => p.Host == null)
|
||||
.Select(p => new EmojiResponse
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Uri = p.Uri,
|
||||
Aliases = p.Aliases,
|
||||
Category = p.Category,
|
||||
PublicUrl = p.PublicUrl,
|
||||
License = p.License
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(EmojiResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetEmoji(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<EmojiResponse> GetEmoji(string id)
|
||||
{
|
||||
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id);
|
||||
var emoji = await db.Emojis.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.NotFound("Emoji not found");
|
||||
|
||||
if (emoji == null) return NotFound();
|
||||
|
||||
var res = new EmojiResponse
|
||||
return new EmojiResponse
|
||||
{
|
||||
Id = emoji.Id,
|
||||
Name = emoji.Name,
|
||||
|
@ -61,19 +60,17 @@ public class EmojiController(
|
|||
PublicUrl = emoji.PublicUrl,
|
||||
License = emoji.License
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("role:admin")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(EmojiResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> UploadEmoji(IFormFile file)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.Conflict)]
|
||||
public async Task<EmojiResponse> UploadEmoji(IFormFile file)
|
||||
{
|
||||
var emoji = await emojiSvc.CreateEmojiFromStream(file.OpenReadStream(), file.FileName, file.ContentType);
|
||||
|
||||
var res = new EmojiResponse
|
||||
return new EmojiResponse
|
||||
{
|
||||
Id = emoji.Id,
|
||||
Name = emoji.Name,
|
||||
|
@ -83,25 +80,22 @@ public class EmojiController(
|
|||
PublicUrl = emoji.PublicUrl,
|
||||
License = null
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("clone/{name}@{host}")]
|
||||
[Authorize("role:admin")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(EmojiResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> CloneEmoji(string name, string host)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound, HttpStatusCode.Conflict)]
|
||||
public async Task<EmojiResponse> CloneEmoji(string name, string host)
|
||||
{
|
||||
var localEmojo = await db.Emojis.FirstOrDefaultAsync(e => e.Name == name && e.Host == null);
|
||||
if (localEmojo != null) return Conflict();
|
||||
if (localEmojo != null) throw GracefulException.Conflict("An emoji with that name already exists");
|
||||
|
||||
var emojo = await db.Emojis.FirstOrDefaultAsync(e => e.Name == name && e.Host == host);
|
||||
if (emojo == null) return NotFound();
|
||||
if (emojo == null) throw GracefulException.NotFound("Emoji not found");
|
||||
|
||||
var cloned = await emojiSvc.CloneEmoji(emojo);
|
||||
var response = new EmojiResponse
|
||||
var cloned = await emojiSvc.CloneEmoji(emojo);
|
||||
return new EmojiResponse
|
||||
{
|
||||
Id = cloned.Id,
|
||||
Name = cloned.Name,
|
||||
|
@ -111,33 +105,30 @@ public class EmojiController(
|
|||
PublicUrl = cloned.PublicUrl,
|
||||
License = null
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
[Authorize("role:admin")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
public async Task<IActionResult> ImportEmoji(IFormFile file)
|
||||
[ProducesResults(HttpStatusCode.Accepted)]
|
||||
public async Task<AcceptedResult> ImportEmoji(IFormFile file)
|
||||
{
|
||||
var zip = await emojiImportSvc.Parse(file.OpenReadStream());
|
||||
await emojiImportSvc.Import(zip); // TODO: run in background. this will take a while
|
||||
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[Authorize("role:admin")]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(EmojiResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> UpdateEmoji(string id, UpdateEmojiRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<EmojiResponse> UpdateEmoji(string id, UpdateEmojiRequest request)
|
||||
{
|
||||
var emoji = await emojiSvc.UpdateLocalEmoji(id, request.Name, request.Aliases, request.Category,
|
||||
request.License);
|
||||
if (emoji == null) return NotFound();
|
||||
request.License) ??
|
||||
throw GracefulException.NotFound("Emoji not found");
|
||||
|
||||
var res = new EmojiResponse
|
||||
return new EmojiResponse
|
||||
{
|
||||
Id = emoji.Id,
|
||||
Name = emoji.Name,
|
||||
|
@ -147,17 +138,14 @@ public class EmojiController(
|
|||
PublicUrl = emoji.PublicUrl,
|
||||
License = emoji.License
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize("role:admin")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> DeleteEmoji(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task DeleteEmoji(string id)
|
||||
{
|
||||
await emojiSvc.DeleteEmoji(id);
|
||||
return Ok();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -11,10 +11,10 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
|||
public class FallbackController : ControllerBase
|
||||
{
|
||||
[EnableCors("fallback")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(ErrorResponse))]
|
||||
[ProducesErrors(HttpStatusCode.NotImplemented)]
|
||||
public IActionResult FallbackAction()
|
||||
{
|
||||
throw new GracefulException(HttpStatusCode.NotImplemented,
|
||||
"This API method has not been implemented", Request.Path);
|
||||
throw new GracefulException(HttpStatusCode.NotImplemented, "This API method has not been implemented",
|
||||
Request.Path);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
||||
|
@ -27,8 +28,8 @@ public class FollowRequestController(
|
|||
{
|
||||
[HttpGet]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<FollowRequestResponse>))]
|
||||
public async Task<IActionResult> GetFollowRequests(PaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<FollowRequestResponse>> GetFollowRequests(PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var requests = await db.FollowRequests
|
||||
|
@ -39,17 +40,16 @@ public class FollowRequestController(
|
|||
.ToListAsync();
|
||||
|
||||
var users = await userRenderer.RenderMany(requests.Select(p => p.Follower));
|
||||
var res = requests.Select(p => new FollowRequestResponse
|
||||
return requests.Select(p => new FollowRequestResponse
|
||||
{
|
||||
Id = p.Id, User = users.First(u => u.Id == p.Follower.Id)
|
||||
});
|
||||
return Ok(res.ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/accept")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> AcceptFollowRequest(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task AcceptFollowRequest(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var request = await db.FollowRequests
|
||||
|
@ -58,13 +58,12 @@ public class FollowRequestController(
|
|||
throw GracefulException.NotFound("Follow request not found");
|
||||
|
||||
await userSvc.AcceptFollowRequestAsync(request);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reject")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> RejectFollowRequest(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task RejectFollowRequest(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var request = await db.FollowRequests
|
||||
|
@ -73,6 +72,5 @@ public class FollowRequestController(
|
|||
throw GracefulException.NotFound("Follow request not found");
|
||||
|
||||
await userSvc.RejectFollowRequestAsync(request);
|
||||
return Ok();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
using System.IO.Hashing;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SixLabors.ImageSharp;
|
||||
|
@ -15,10 +17,10 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
|||
[Route("/identicon/{id}")]
|
||||
[Route("/identicon/{id}.png")]
|
||||
[Produces(MediaTypeNames.Image.Png)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
public class IdenticonController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task GetIdenticon(string id)
|
||||
{
|
||||
using var image = new Image<Rgb24>(Size, Size);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using AsyncKeyedLock;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
|
@ -37,9 +38,9 @@ public class NoteController(
|
|||
|
||||
[HttpGet("{id}")]
|
||||
[Authenticate]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NoteResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<NoteResponse> GetNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -50,29 +51,28 @@ public class NoteController(
|
|||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.NotFound("Note not found");
|
||||
|
||||
return Ok(await noteRenderer.RenderOne(note.EnforceRenoteReplyVisibility(), user));
|
||||
return await noteRenderer.RenderOne(note.EnforceRenoteReplyVisibility(), user);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> DeleteNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task DeleteNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.FirstOrDefaultAsync(p => p.Id == id && p.User == user) ??
|
||||
throw GracefulException.NotFound("Note not found");
|
||||
|
||||
await noteSvc.DeleteNoteAsync(note);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/ascendants")]
|
||||
[Authenticate]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<NoteResponse>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteAscendants(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetNoteAscendants(
|
||||
string id, [FromQuery] [DefaultValue(20)] [Range(1, 100)] int? limit
|
||||
)
|
||||
{
|
||||
|
@ -94,14 +94,14 @@ public class NoteController(
|
|||
var res = await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Threads);
|
||||
|
||||
return Ok(res.ToList().OrderAncestors());
|
||||
return res.ToList().OrderAncestors();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/descendants")]
|
||||
[Authenticate]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<NoteResponse>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteDescendants(
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetNoteDescendants(
|
||||
string id, [FromQuery] [DefaultValue(20)] [Range(1, 100)] int? depth
|
||||
)
|
||||
{
|
||||
|
@ -123,16 +123,16 @@ public class NoteController(
|
|||
|
||||
var notes = hits.EnforceRenoteReplyVisibility();
|
||||
var res = await noteRenderer.RenderMany(notes, user, Filter.FilterContext.Threads);
|
||||
return Ok(res.ToList().OrderDescendants());
|
||||
return res.ToList().OrderDescendants();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/reactions/{name}")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[LinkPagination(20, 40)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<UserResponse>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetNoteReactions(string id, string name)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<UserResponse>> GetNoteReactions(string id, string name)
|
||||
{
|
||||
var user = HttpContext.GetUser();
|
||||
var note = await db.Notes
|
||||
|
@ -147,15 +147,15 @@ public class NoteController(
|
|||
.Select(p => p.User)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(await userRenderer.RenderMany(users));
|
||||
return await userRenderer.RenderMany(users);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/like")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ValueResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> LikeNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ValueResponse> LikeNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes
|
||||
|
@ -167,15 +167,15 @@ public class NoteController(
|
|||
|
||||
var success = await noteSvc.LikeNoteAsync(note, user);
|
||||
|
||||
return Ok(new ValueResponse(success ? ++note.LikeCount : note.LikeCount));
|
||||
return new ValueResponse(success ? ++note.LikeCount : note.LikeCount);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unlike")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ValueResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> UnlikeNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ValueResponse> UnlikeNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes
|
||||
|
@ -187,15 +187,15 @@ public class NoteController(
|
|||
|
||||
var success = await noteSvc.UnlikeNoteAsync(note, user);
|
||||
|
||||
return Ok(new ValueResponse(success ? --note.LikeCount : note.LikeCount));
|
||||
return new ValueResponse(success ? --note.LikeCount : note.LikeCount);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/renote")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ValueResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> RenoteNote(string id, [FromQuery] NoteVisibility? visibility = null)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ValueResponse> RenoteNote(string id, [FromQuery] NoteVisibility? visibility = null)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes
|
||||
|
@ -206,15 +206,15 @@ public class NoteController(
|
|||
throw GracefulException.NotFound("Note not found");
|
||||
|
||||
var success = await noteSvc.RenoteNoteAsync(note, user, (Note.NoteVisibility?)visibility);
|
||||
return Ok(new ValueResponse(success != null ? ++note.RenoteCount : note.RenoteCount));
|
||||
return new ValueResponse(success != null ? ++note.RenoteCount : note.RenoteCount);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unrenote")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ValueResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> UnrenoteNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ValueResponse> UnrenoteNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes
|
||||
|
@ -225,15 +225,15 @@ public class NoteController(
|
|||
throw GracefulException.NotFound("Note not found");
|
||||
|
||||
var count = await noteSvc.UnrenoteNoteAsync(note, user);
|
||||
return Ok(new ValueResponse(note.RenoteCount - count));
|
||||
return new ValueResponse(note.RenoteCount - count);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/react/{name}")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ValueResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> ReactToNote(string id, string name)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ValueResponse> ReactToNote(string id, string name)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes
|
||||
|
@ -245,15 +245,15 @@ public class NoteController(
|
|||
|
||||
var res = await noteSvc.ReactToNoteAsync(note, user, name);
|
||||
note.Reactions.TryGetValue(res.name, out var count);
|
||||
return Ok(new ValueResponse(res.success ? ++count : count));
|
||||
return new ValueResponse(res.success ? ++count : count);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unreact/{name}")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ValueResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> RemoveReactionFromNote(string id, string name)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<ValueResponse> RemoveReactionFromNote(string id, string name)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id)
|
||||
|
@ -264,16 +264,16 @@ public class NoteController(
|
|||
|
||||
var res = await noteSvc.RemoveReactionFromNoteAsync(note, user, name);
|
||||
note.Reactions.TryGetValue(res.name, out var count);
|
||||
return Ok(new ValueResponse(res.success ? --count : count));
|
||||
return new ValueResponse(res.success ? --count : count);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/refetch")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[EnableRateLimiting("strict")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NoteRefetchResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> RefetchNote(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<NoteRefetchResponse> RefetchNote(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var note = await db.Notes.Where(p => p.Id == id && p.User.Host != null && p.Uri != null)
|
||||
|
@ -330,19 +330,19 @@ public class NoteController(
|
|||
.FirstOrDefaultAsync() ??
|
||||
throw new Exception("Note disappeared during refetch");
|
||||
|
||||
var res = new NoteRefetchResponse
|
||||
return new NoteRefetchResponse
|
||||
{
|
||||
Note = await noteRenderer.RenderOne(note.EnforceRenoteReplyVisibility(), user), Errors = errors
|
||||
};
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NoteResponse))]
|
||||
public async Task<IActionResult> CreateNote(NoteCreateRequest request)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<NoteResponse> CreateNote(NoteCreateRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
|
||||
|
@ -397,6 +397,6 @@ public class NoteController(
|
|||
if (request.IdempotencyKey != null)
|
||||
await cache.SetAsync($"idempotency:{user.Id}:{request.IdempotencyKey}", note.Id, TimeSpan.FromHours(24));
|
||||
|
||||
return Ok(await noteRenderer.RenderOne(note, user));
|
||||
return await noteRenderer.RenderOne(note, user);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
||||
|
@ -22,8 +23,8 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
{
|
||||
[HttpGet]
|
||||
[LinkPagination(20, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NotificationResponse>))]
|
||||
public async Task<IActionResult> GetNotifications(PaginationQuery query)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NotificationResponse>> GetNotifications(PaginationQuery query)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notifications = await db.Notifications
|
||||
|
@ -35,13 +36,13 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
.PrecomputeNoteVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(await notificationRenderer.RenderMany(notifications.EnforceRenoteReplyVisibility(p => p.Note), user));
|
||||
return await notificationRenderer.RenderMany(notifications.EnforceRenoteReplyVisibility(p => p.Note), user);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/read")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> MarkNotificationAsRead(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task MarkNotificationAsRead(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notification = await db.Notifications.FirstOrDefaultAsync(p => p.Notifiee == user && p.Id == id) ??
|
||||
|
@ -52,25 +53,21 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
notification.IsRead = true;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
|
||||
[HttpPost("read")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
public async Task<IActionResult> MarkAllNotificationsAsRead()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task MarkAllNotificationsAsRead()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
await db.Notifications.Where(p => p.Notifiee == user && !p.IsRead)
|
||||
.ExecuteUpdateAsync(p => p.SetProperty(n => n.IsRead, true));
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> DeleteNotification(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task DeleteNotification(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notification = await db.Notifications.FirstOrDefaultAsync(p => p.Notifiee == user && p.Id == id) ??
|
||||
|
@ -78,18 +75,14 @@ public class NotificationController(DatabaseContext db, NotificationRenderer not
|
|||
|
||||
db.Remove(notification);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
public async Task<IActionResult> DeleteAllNotifications()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task DeleteAllNotifications()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
await db.Notifications.Where(p => p.Notifiee == user)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
||||
|
@ -30,13 +31,14 @@ public class SearchController(
|
|||
UserRenderer userRenderer,
|
||||
ActivityPub.UserResolver userResolver,
|
||||
IOptions<Config.InstanceSection> config
|
||||
)
|
||||
: ControllerBase
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("notes")]
|
||||
[LinkPagination(20, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<NoteResponse>))]
|
||||
public async Task<IActionResult> SearchNotes([FromQuery(Name = "q")] string query, PaginationQuery pagination)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> SearchNotes(
|
||||
[FromQuery(Name = "q")] string query, PaginationQuery pagination
|
||||
)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notes = await db.Notes
|
||||
|
@ -48,15 +50,17 @@ public class SearchController(
|
|||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), user));
|
||||
return await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), user);
|
||||
}
|
||||
|
||||
[HttpGet("users")]
|
||||
[LinkPagination(20, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<UserResponse>))]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall",
|
||||
Justification = "Inspection doesn't know about the Projectable attribute")]
|
||||
public async Task<IActionResult> SearchUsers([FromQuery(Name = "q")] string query, PaginationQuery pagination)
|
||||
public async Task<IEnumerable<UserResponse>> SearchUsers(
|
||||
[FromQuery(Name = "q")] string query, PaginationQuery pagination
|
||||
)
|
||||
{
|
||||
var users = await db.Users
|
||||
.IncludeCommonProperties()
|
||||
|
@ -66,14 +70,13 @@ public class SearchController(
|
|||
.OrderByDescending(p => p.NotesCount)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(await userRenderer.RenderMany(users));
|
||||
return await userRenderer.RenderMany(users);
|
||||
}
|
||||
|
||||
[HttpGet("lookup")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RedirectResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> Lookup([FromQuery(Name = "target")] string target)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||
public async Task<RedirectResponse> Lookup([FromQuery(Name = "target")] string target)
|
||||
{
|
||||
target = target.Trim();
|
||||
|
||||
|
@ -85,7 +88,7 @@ public class SearchController(
|
|||
if (target.StartsWith('@') || target.StartsWith(userPrefixAlt))
|
||||
{
|
||||
var hit = await userResolver.ResolveAsyncOrNull(target);
|
||||
if (hit != null) return Ok(new RedirectResponse { TargetUrl = $"/users/{hit.Id}" });
|
||||
if (hit != null) return new RedirectResponse { TargetUrl = $"/users/{hit.Id}" };
|
||||
throw GracefulException.NotFound("No result found");
|
||||
}
|
||||
|
||||
|
@ -112,18 +115,18 @@ public class SearchController(
|
|||
}
|
||||
|
||||
noteHit ??= await db.Notes.FirstOrDefaultAsync(p => p.Uri == target || p.Url == target);
|
||||
if (noteHit != null) return Ok(new RedirectResponse { TargetUrl = $"/notes/{noteHit.Id}" });
|
||||
if (noteHit != null) return new RedirectResponse { TargetUrl = $"/notes/{noteHit.Id}" };
|
||||
|
||||
userHit ??= await db.Users.FirstOrDefaultAsync(p => p.Uri == target ||
|
||||
(p.UserProfile != null &&
|
||||
p.UserProfile.Url == target));
|
||||
if (userHit != null) return Ok(new RedirectResponse { TargetUrl = $"/users/{userHit.Id}" });
|
||||
if (userHit != null) return new RedirectResponse { TargetUrl = $"/users/{userHit.Id}" };
|
||||
|
||||
noteHit = await noteSvc.ResolveNoteAsync(target);
|
||||
if (noteHit != null) return Ok(new RedirectResponse { TargetUrl = $"/notes/{noteHit.Id}" });
|
||||
if (noteHit != null) return new RedirectResponse { TargetUrl = $"/notes/{noteHit.Id}" };
|
||||
|
||||
userHit = await userResolver.ResolveAsyncOrNull(target);
|
||||
if (userHit != null) return Ok(new RedirectResponse { TargetUrl = $"/users/{userHit.Id}" });
|
||||
if (userHit != null) return new RedirectResponse { TargetUrl = $"/users/{userHit.Id}" };
|
||||
|
||||
throw GracefulException.NotFound("No result found");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
|
@ -17,26 +19,23 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
|||
public class SettingsController(DatabaseContext db) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserSettingsEntity))]
|
||||
public async Task<IActionResult> GetSettings()
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<UserSettingsEntity> GetSettings()
|
||||
{
|
||||
var settings = await GetOrInitUserSettings();
|
||||
|
||||
var res = new UserSettingsEntity
|
||||
return new UserSettingsEntity
|
||||
{
|
||||
FilterInaccessible = settings.FilterInaccessible,
|
||||
PrivateMode = settings.PrivateMode,
|
||||
DefaultNoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility,
|
||||
DefaultRenoteVisibility = (NoteVisibility)settings.DefaultNoteVisibility
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(object))]
|
||||
public async Task<IActionResult> UpdateSettings(UserSettingsEntity newSettings)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task UpdateSettings(UserSettingsEntity newSettings)
|
||||
{
|
||||
var settings = await GetOrInitUserSettings();
|
||||
|
||||
|
@ -46,21 +45,18 @@ public class SettingsController(DatabaseContext db) : ControllerBase
|
|||
settings.DefaultRenoteVisibility = (Note.NoteVisibility)newSettings.DefaultRenoteVisibility;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new object());
|
||||
}
|
||||
|
||||
private async Task<UserSettings> GetOrInitUserSettings()
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var settings = user.UserSettings;
|
||||
if (settings == null)
|
||||
{
|
||||
settings = new UserSettings { User = user };
|
||||
db.Add(settings);
|
||||
await db.SaveChangesAsync();
|
||||
await db.ReloadEntityAsync(settings);
|
||||
}
|
||||
if (settings != null) return settings;
|
||||
|
||||
settings = new UserSettings { User = user };
|
||||
db.Add(settings);
|
||||
await db.SaveChangesAsync();
|
||||
await db.ReloadEntityAsync(settings);
|
||||
return settings;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
||||
|
@ -24,9 +25,8 @@ public class TimelineController(DatabaseContext db, NoteRenderer noteRenderer, C
|
|||
[HttpGet("home")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<NoteResponse>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetHomeTimeline(PaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetHomeTimeline(PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var heuristic = await QueryableTimelineExtensions.GetHeuristic(user, db, cache);
|
||||
|
@ -38,6 +38,6 @@ public class TimelineController(DatabaseContext db, NoteRenderer noteRenderer, C
|
|||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), user, Filter.FilterContext.Home));
|
||||
return await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), user, Filter.FilterContext.Home);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
||||
|
@ -33,21 +34,21 @@ public class UserController(
|
|||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<UserResponse> GetUser(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
|
||||
return Ok(await userRenderer.RenderOne(await userResolver.GetUpdatedUser(user)));
|
||||
return await userRenderer.RenderOne(await userResolver.GetUpdatedUser(user));
|
||||
}
|
||||
|
||||
[HttpGet("lookup")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> LookupUser([FromQuery] string username, [FromQuery] string? host)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<UserResponse> LookupUser([FromQuery] string username, [FromQuery] string? host)
|
||||
{
|
||||
username = username.ToLowerInvariant();
|
||||
host = host?.ToLowerInvariant();
|
||||
|
@ -59,27 +60,27 @@ public class UserController(
|
|||
.FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == host) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
|
||||
return Ok(await userRenderer.RenderOne(await userResolver.GetUpdatedUser(user)));
|
||||
return await userRenderer.RenderOne(await userResolver.GetUpdatedUser(user));
|
||||
}
|
||||
|
||||
[HttpGet("{id}/profile")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserProfileResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetUserProfile(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<UserProfileResponse> GetUserProfile(string id)
|
||||
{
|
||||
var localUser = HttpContext.GetUserOrFail();
|
||||
var user = await db.Users.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
|
||||
return Ok(await userProfileRenderer.RenderOne(await userResolver.GetUpdatedUser(user), localUser));
|
||||
return await userProfileRenderer.RenderOne(await userResolver.GetUpdatedUser(user), localUser);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/notes")]
|
||||
[LinkPagination(20, 80)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<NoteResponse>))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetUserNotes(string id, PaginationQuery pq)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetUserNotes(string id, PaginationQuery pq)
|
||||
{
|
||||
var localUser = HttpContext.GetUserOrFail();
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
|
@ -92,20 +93,18 @@ public class UserController(
|
|||
.FilterHidden(localUser, db, filterMutes: false)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.PrecomputeVisibilities(localUser)
|
||||
.ToListAsync();
|
||||
.ToListAsync()
|
||||
.ContinueWith(n => n.Result.EnforceRenoteReplyVisibility());
|
||||
|
||||
return Ok(await noteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility(), localUser,
|
||||
Filter.FilterContext.Accounts));
|
||||
return await noteRenderer.RenderMany(notes, localUser, Filter.FilterContext.Accounts);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/follow")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> FollowUser(string id)
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.Forbidden, HttpStatusCode.NotFound)]
|
||||
public async Task FollowUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -115,22 +114,18 @@ public class UserController(
|
|||
.Where(p => p.Id == id)
|
||||
.PrecomputeRelationshipData(user)
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw GracefulException.RecordNotFound();
|
||||
throw GracefulException.NotFound("User not found");
|
||||
|
||||
if ((followee.PrecomputedIsBlockedBy ?? true) || (followee.PrecomputedIsBlocking ?? true))
|
||||
throw GracefulException.Forbidden("This action is not allowed");
|
||||
|
||||
if (!(followee.PrecomputedIsFollowedBy ?? false) && !(followee.PrecomputedIsRequestedBy ?? false))
|
||||
await userSvc.FollowUserAsync(user, followee);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/unfollow")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> UnfollowUser(string id)
|
||||
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||
public async Task UnfollowUser(string id)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
|
@ -144,7 +139,5 @@ public class UserController(
|
|||
throw GracefulException.RecordNotFound();
|
||||
|
||||
await userSvc.UnfollowUserAsync(user, followee);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
|
@ -2,8 +2,12 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Reflection;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
@ -18,8 +22,11 @@ public static class SwaggerGenOptionsExtensions
|
|||
options.SupportNonNullableReferenceTypes(); // Sets Nullable flags appropriately.
|
||||
options.UseAllOfToExtendReferenceSchemas(); // Allows $ref enums to be nullable
|
||||
options.UseAllOfForInheritance(); // Allows $ref objects to be nullable
|
||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||
options.OperationFilter<AuthorizeCheckOperationDocumentFilter>();
|
||||
options.OperationFilter<HybridRequestOperationFilter>();
|
||||
options.OperationFilter<PossibleErrorsOperationFilter>();
|
||||
options.OperationFilter<PossibleResultsOperationFilter>();
|
||||
options.DocumentFilter<AuthorizeCheckOperationDocumentFilter>();
|
||||
options.DocInclusionPredicate(DocInclusionPredicate);
|
||||
}
|
||||
|
||||
|
@ -63,8 +70,91 @@ public static class SwaggerGenOptionsExtensions
|
|||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
|
||||
Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")]
|
||||
private class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
private class AuthorizeCheckOperationDocumentFilter : IOperationFilter, IDocumentFilter
|
||||
{
|
||||
private const string Web401 =
|
||||
"""
|
||||
{
|
||||
"statusCode": 401,
|
||||
"error": "Unauthorized",
|
||||
"message": "This method requires an authenticated user"
|
||||
}
|
||||
""";
|
||||
|
||||
private const string Web403 =
|
||||
"""
|
||||
{
|
||||
"statusCode": 403,
|
||||
"error": "Forbidden",
|
||||
"message": "This action is outside the authorized scopes"
|
||||
}
|
||||
""";
|
||||
|
||||
private const string Masto401 =
|
||||
"""
|
||||
{
|
||||
"error": "This method requires an authenticated user"
|
||||
}
|
||||
""";
|
||||
|
||||
private const string Masto403 =
|
||||
"""
|
||||
{
|
||||
"message": "This action is outside the authorized scopes"
|
||||
}
|
||||
""";
|
||||
|
||||
private static readonly OpenApiString MastoExample401 = new(Masto401);
|
||||
private static readonly OpenApiString MastoExample403 = new(Masto403);
|
||||
private static readonly OpenApiString WebExample401 = new(Web401);
|
||||
private static readonly OpenApiString WebExample403 = new(Web403);
|
||||
|
||||
private static readonly OpenApiReference Ref401 =
|
||||
new() { Type = ReferenceType.Response, Id = "error-401" };
|
||||
|
||||
private static readonly OpenApiReference Ref403 =
|
||||
new() { Type = ReferenceType.Response, Id = "error-403" };
|
||||
|
||||
private static readonly OpenApiResponse MastoRes401 = new()
|
||||
{
|
||||
Reference = Ref401,
|
||||
Description = "Unauthorized",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = MastoExample401 } }
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly OpenApiResponse MastoRes403 = new()
|
||||
{
|
||||
Reference = Ref403,
|
||||
Description = "Forbidden",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = MastoExample403 } }
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly OpenApiResponse WebRes401 = new()
|
||||
{
|
||||
Reference = Ref401,
|
||||
Description = "Unauthorized",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = WebExample401 } }
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly OpenApiResponse WebRes403 = new()
|
||||
{
|
||||
Reference = Ref403,
|
||||
Description = "Forbidden",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = WebExample403 } }
|
||||
}
|
||||
};
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (context.MethodInfo.DeclaringType is null)
|
||||
|
@ -102,69 +192,125 @@ public static class SwaggerGenOptionsExtensions
|
|||
|
||||
if (authorizeAttribute == null) return;
|
||||
|
||||
const string web401 =
|
||||
"""
|
||||
{
|
||||
"statusCode": 401,
|
||||
"error": "Unauthorized",
|
||||
"message": "This method requires an authenticated user"
|
||||
}
|
||||
""";
|
||||
|
||||
const string web403 =
|
||||
"""
|
||||
{
|
||||
"statusCode": 403,
|
||||
"error": "Forbidden",
|
||||
"message": "This action is outside the authorized scopes"
|
||||
}
|
||||
""";
|
||||
|
||||
const string masto401 =
|
||||
"""
|
||||
{
|
||||
"error": "This method requires an authenticated user"
|
||||
}
|
||||
""";
|
||||
|
||||
const string masto403 =
|
||||
"""
|
||||
{
|
||||
"message": "This action is outside the authorized scopes"
|
||||
}
|
||||
""";
|
||||
|
||||
var example401 = new OpenApiString(isMastodonController ? masto401 : web401);
|
||||
|
||||
var res401 = new OpenApiResponse
|
||||
{
|
||||
Description = "Unauthorized",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = example401 } }
|
||||
}
|
||||
};
|
||||
|
||||
operation.Responses.Remove("401");
|
||||
operation.Responses.Add("401", res401);
|
||||
operation.Responses.Add("401", new OpenApiResponse { Reference = Ref401 });
|
||||
|
||||
if (authorizeAttribute is { AdminRole: false, ModeratorRole: false, Scopes.Length: 0 } &&
|
||||
authenticateAttribute is { AdminRole: false, ModeratorRole: false, Scopes.Length: 0 })
|
||||
return;
|
||||
|
||||
operation.Responses.Remove("403");
|
||||
operation.Responses.Add("403", new OpenApiResponse { Reference = Ref403 });
|
||||
}
|
||||
|
||||
var example403 = new OpenApiString(isMastodonController ? masto403 : web403);
|
||||
|
||||
var res403 = new OpenApiResponse
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (swaggerDoc.Info.Title == "Mastodon")
|
||||
{
|
||||
Description = "Forbidden",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
swaggerDoc.Components.Responses.Add(Ref401.Id, MastoRes401);
|
||||
swaggerDoc.Components.Responses.Add(Ref403.Id, MastoRes403);
|
||||
}
|
||||
else
|
||||
{
|
||||
swaggerDoc.Components.Responses.Add(Ref401.Id, WebRes401);
|
||||
swaggerDoc.Components.Responses.Add(Ref403.Id, WebRes403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
|
||||
Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")]
|
||||
private class PossibleErrorsOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (context.MethodInfo.DeclaringType is null)
|
||||
return;
|
||||
|
||||
var attribute = context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<ProducesErrorsAttribute>()
|
||||
.FirstOrDefault() ??
|
||||
context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<ProducesErrorsAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (attribute == null) return;
|
||||
|
||||
var isMastodonController = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<MastodonApiControllerAttribute>()
|
||||
.Any();
|
||||
|
||||
var type = isMastodonController ? typeof(MastodonErrorResponse) : typeof(ErrorResponse);
|
||||
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
|
||||
|
||||
foreach (var status in attribute.StatusCodes.Distinct())
|
||||
{
|
||||
var res = new OpenApiResponse
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Example = example403 } }
|
||||
}
|
||||
};
|
||||
operation.Responses.Add("403", res403);
|
||||
Description = ReasonPhrases.GetReasonPhrase((int)status),
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
{ "application/json", new OpenApiMediaType { Schema = schema } }
|
||||
}
|
||||
};
|
||||
|
||||
operation.Responses.Remove(((int)status).ToString());
|
||||
operation.Responses.Add(((int)status).ToString(), res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
|
||||
Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")]
|
||||
private class PossibleResultsOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (context.MethodInfo.DeclaringType is null)
|
||||
return;
|
||||
|
||||
var attribute = context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<ProducesResultsAttribute>()
|
||||
.FirstOrDefault() ??
|
||||
context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<ProducesResultsAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (attribute == null) return;
|
||||
|
||||
var overrideType = context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<OverrideResultTypeAttribute>()
|
||||
.FirstOrDefault() ??
|
||||
context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<OverrideResultTypeAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
var type = overrideType?.Type ??
|
||||
context.ApiDescription.SupportedResponseTypes.FirstOrDefault(p => p.Type != typeof(void))?.Type;
|
||||
|
||||
var schema = type != null
|
||||
? context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository)
|
||||
: null;
|
||||
|
||||
var openApiMediaType = new OpenApiMediaType { Schema = schema };
|
||||
foreach (var status in attribute.StatusCodes.Distinct())
|
||||
{
|
||||
var content = schema != null
|
||||
? context.ApiDescription.SupportedResponseTypes
|
||||
.Where(p => p.StatusCode == (int)status)
|
||||
.SelectMany(p => p.ApiResponseFormats.Select(i => i.MediaType))
|
||||
.Distinct()
|
||||
.ToDictionary(contentType => contentType, _ => openApiMediaType)
|
||||
: null;
|
||||
|
||||
var res = new OpenApiResponse
|
||||
{
|
||||
Description = ReasonPhrases.GetReasonPhrase((int)status), Content = content
|
||||
};
|
||||
|
||||
operation.Responses.Remove(((int)status).ToString());
|
||||
operation.Responses.Add(((int)status).ToString(), res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -114,27 +114,29 @@ public static class LdHelpers
|
|||
return payload;
|
||||
}
|
||||
|
||||
public static JObject? Compact(this ASObject obj)
|
||||
public static JObject Compact(this ASObject obj)
|
||||
{
|
||||
return Compact((object)obj);
|
||||
return Compact((object)obj) ?? throw new Exception("Failed to compact JSON-LD paylaod");
|
||||
}
|
||||
|
||||
public static JObject? Compact(object obj)
|
||||
public static JObject Compact(object obj)
|
||||
{
|
||||
return Compact(JToken.FromObject(obj, JsonSerializer));
|
||||
return Compact(JToken.FromObject(obj, JsonSerializer)) ??
|
||||
throw new Exception("Failed to compact JSON-LD paylaod");
|
||||
}
|
||||
|
||||
public static JArray? Expand(object obj)
|
||||
public static JArray Expand(object obj)
|
||||
{
|
||||
return Expand(JToken.FromObject(obj, JsonSerializer));
|
||||
return Expand(JToken.FromObject(obj, JsonSerializer)) ??
|
||||
throw new Exception("Failed to expand JSON-LD paylaod");
|
||||
}
|
||||
|
||||
public static JObject? Compact(JToken? json)
|
||||
public static JObject Compact(JToken? json)
|
||||
{
|
||||
return JsonLdProcessor.Compact(json, FederationContext, Options);
|
||||
}
|
||||
|
||||
public static JArray? Expand(JToken? json)
|
||||
public static JArray Expand(JToken? json)
|
||||
{
|
||||
return JsonLdProcessor.Expand(json, Options);
|
||||
}
|
||||
|
|
|
@ -77,13 +77,13 @@ public static class LdSignature
|
|||
|
||||
activity.Add($"{Constants.W3IdSecurityNs}#signature", JToken.FromObject(options));
|
||||
|
||||
return LdHelpers.Expand(activity)?[0] as JObject ??
|
||||
return LdHelpers.Expand(activity)[0] as JObject ??
|
||||
throw new GracefulException(HttpStatusCode.UnprocessableEntity, "Failed to expand signed activity");
|
||||
}
|
||||
|
||||
private static Task<byte[]?> GetSignatureDataAsync(JToken data, SignatureOptions options)
|
||||
{
|
||||
return GetSignatureDataAsync(data, LdHelpers.Expand(JObject.FromObject(options))!);
|
||||
return GetSignatureDataAsync(data, LdHelpers.Expand(JObject.FromObject(options)));
|
||||
}
|
||||
|
||||
private static async Task<byte[]?> GetSignatureDataAsync(JToken data, JToken options)
|
||||
|
|
|
@ -367,6 +367,79 @@
|
|||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Description/@EntryValue">ProducesResultsAndErrors</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Field/=BadRequest/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Field/=BadRequest/Order/@EntryValue">1</s:Int64>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Field/=OK/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Field/=OK/Order/@EntryValue">0</s:Int64>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Shortcut/@EntryValue">prese</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=82C6671DFF72A14F823A7B0C48905556/Text/@EntryValue">[ProducesResults(HttpStatusCode.$OK$)]
|
||||
[ProducesErrors(HttpStatusCode.$BadRequest$)]$SELECTION$$END$</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Description/@EntryValue">ProducesResults</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Field/=OK/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Field/=OK/Order/@EntryValue">0</s:Int64>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Field/=RES/@KeyIndexDefined">False</s:Boolean>
|
||||
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Reformat/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Shortcut/@EntryValue">pres</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=8BD853BBFDD9844391296D061B40C3C1/Text/@EntryValue">[ProducesResults(HttpStatusCode.$OK$)]$SELECTION$$END$</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Description/@EntryValue">ProducesOkResult</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Shortcut/@EntryValue">pro</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9629E918AE752F4DABD159FCB795983B/Text/@EntryValue">[ProducesResults(HttpStatusCode.OK)]$SELECTION$$END$</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Description/@EntryValue">ProducesOkResult</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Shortcut/@EntryValue">presok</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=9E19E30B0BA8D0449B8950655FAC5118/Text/@EntryValue">[ProducesResults(HttpStatusCode.OK)]$SELECTION$$END$</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Description/@EntryValue">OverrideResultType</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Field/=object/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Field/=object/Order/@EntryValue">0</s:Int64>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Shortcut/@EntryValue">ort</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B90DFEF27AA100438EDC5CE3D174909B/Text/@EntryValue">[OverrideResultType<$object$>]$SELECTION$$END$</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Description/@EntryValue">ProducesErrors</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Field/=BadRequest/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Field/=BadRequest/Order/@EntryValue">0</s:Int64>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Field/=CODE/@KeyIndexDefined">False</s:Boolean>
|
||||
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Field/=ERR/@KeyIndexDefined">False</s:Boolean>
|
||||
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Reformat/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/@KeyIndexDefined">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Scope/=B68999B9D6B43E47A02B22C12A54C3CC/Type/@EntryValue">InCSharpTypeMember</s:String>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Shortcut/@EntryValue">perr</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=D5FAEF0320AE5E47B3F2B73E55C26611/Text/@EntryValue">[ProducesErrors(HttpStatusCode.$BadRequest$)]$SELECTION$$END$</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=blurhash/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Iceshrimp/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Identicon/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
|
@ -21,7 +21,7 @@ public class HttpSignatureTests
|
|||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
_expanded = LdHelpers.Expand(_actor)!;
|
||||
_expanded = LdHelpers.Expand(_actor);
|
||||
_expanded.Should().NotBeNull();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ public class LdSignatureTests
|
|||
[TestInitialize]
|
||||
public async Task Initialize()
|
||||
{
|
||||
_expanded = LdHelpers.Expand(_actor)!;
|
||||
_expanded = LdHelpers.Expand(_actor);
|
||||
_signed = await LdSignature.SignAsync(_expanded, _keypair.ExportRSAPrivateKeyPem(), _actor.Id + "#main-key");
|
||||
|
||||
_expanded.Should().NotBeNull();
|
||||
|
@ -47,7 +47,7 @@ public class LdSignatureTests
|
|||
data.Should().NotBeNull();
|
||||
|
||||
data.Add("https://example.org/ns#test", JToken.FromObject("value"));
|
||||
var expanded = LdHelpers.Expand(data)!;
|
||||
var expanded = LdHelpers.Expand(data);
|
||||
expanded.Should().NotBeNull();
|
||||
|
||||
var verify = await LdSignature.VerifyAsync(expanded, expanded, _keypair.ExportRSAPublicKeyPem());
|
||||
|
|
|
@ -11,7 +11,7 @@ public class JsonLdTests
|
|||
[TestMethod]
|
||||
public void RoundtripTest()
|
||||
{
|
||||
var expanded = LdHelpers.Expand(_actor)!;
|
||||
var expanded = LdHelpers.Expand(_actor);
|
||||
expanded.Should().NotBeNull();
|
||||
|
||||
var canonicalized = LdHelpers.Canonicalize(expanded);
|
||||
|
@ -20,11 +20,11 @@ public class JsonLdTests
|
|||
var compacted = LdHelpers.Compact(expanded);
|
||||
compacted.Should().NotBeNull();
|
||||
|
||||
var expanded2 = LdHelpers.Expand(compacted)!;
|
||||
var expanded2 = LdHelpers.Expand(compacted);
|
||||
expanded2.Should().NotBeNull();
|
||||
expanded2.Should().BeEquivalentTo(expanded);
|
||||
|
||||
var compacted2 = LdHelpers.Compact(expanded2)!;
|
||||
var compacted2 = LdHelpers.Compact(expanded2);
|
||||
compacted2.Should().NotBeNull();
|
||||
compacted2.Should().BeEquivalentTo(compacted);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue