[backend/api] Add note refetch endpoint (ISH-352)

This commit is contained in:
Laura Hausmann 2024-07-04 18:45:16 +02:00
parent 3316a391d5
commit 374d9d5ebf
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
7 changed files with 106 additions and 16 deletions

View file

@ -38,7 +38,7 @@ public class AuthController(DatabaseContext db, MetaService meta) : ControllerBa
}
[HttpPost("/api/v1/apps")]
[EnableRateLimiting("strict")]
[EnableRateLimiting("auth")]
[ConsumesHybrid]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthSchemas.RegisterAppResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]

View file

@ -42,7 +42,7 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere
[HttpPost("login")]
[HideRequestDuration]
[EnableRateLimiting("strict")]
[EnableRateLimiting("auth")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
@ -87,7 +87,7 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere
}
[HttpPost("register")]
[EnableRateLimiting("strict")]
[EnableRateLimiting("auth")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
@ -104,7 +104,7 @@ public class AuthController(DatabaseContext db, UserService userSvc, UserRendere
[HttpPost("change-password")]
[Authenticate]
[Authorize]
[EnableRateLimiting("strict")]
[EnableRateLimiting("auth")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]

View file

@ -267,6 +267,76 @@ public class NoteController(
return Ok(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)
{
var user = HttpContext.GetUserOrFail();
var note = await db.Notes.Where(p => p.Id == id && p.User.Host != null && p.Uri != null)
.IncludeCommonProperties()
.EnsureVisibleFor(user)
.FilterHidden(user, db, false, false)
.FirstOrDefaultAsync() ??
throw GracefulException.NotFound("Note not found");
if (note.Uri == null)
throw new Exception("note.Uri must not be null at this point");
var errors = new List<string>();
try
{
await noteSvc.ResolveNoteAsync(note.Uri, null, user, clearHistory: true, forceRefresh: true);
}
catch (Exception e)
{
errors.Add($"Failed to refetch note: {e.Message}");
}
if (note.ReplyUri != null)
{
try
{
await noteSvc.ResolveNoteAsync(note.ReplyUri, null, user, clearHistory: true, forceRefresh: true);
}
catch (Exception e)
{
errors.Add($"Failed to fetch reply target: {e.Message}");
}
}
if (note.RenoteUri != null)
{
try
{
await noteSvc.ResolveNoteAsync(note.RenoteUri, null, user, clearHistory: true, forceRefresh: true);
}
catch (Exception e)
{
errors.Add($"Failed to fetch renote target: {e.Message}");
}
}
db.ChangeTracker.Clear();
note = await db.Notes.Where(p => p.Id == id && p.User.Host != null && p.Uri != null)
.IncludeCommonProperties()
.EnsureVisibleFor(user)
.FilterHidden(user, db, false, false)
.PrecomputeVisibilities(user)
.FirstOrDefaultAsync() ??
throw new Exception("Note disappeared during refetch");
var res = new NoteRefetchResponse
{
Note = await noteRenderer.RenderOne(note.EnforceRenoteReplyVisibility(), user), Errors = errors
};
return Ok(res);
}
[HttpPost]
[Authenticate]
[Authorize]

View file

@ -240,10 +240,16 @@ public static class ServiceExtensions
QueueLimit = 0
};
options.AddPolicy("sliding", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(),
options.AddPolicy("sliding", ctx =>
RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(false),
_ => sliding));
options.AddPolicy("strict", ctx => RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(),
options.AddPolicy("auth", ctx =>
RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(false),
_ => strict));
options.AddPolicy("strict", ctx =>
RateLimitPartition.GetSlidingWindowLimiter(ctx.GetRateLimitPartition(true),
_ => strict));
options.OnRejected = async (context, token) =>
@ -316,7 +322,10 @@ public static class ServiceExtensions
public static class HttpContextExtensions
{
public static string? GetRateLimitPartition(this HttpContext ctx) =>
public static string GetRateLimitPartition(this HttpContext ctx, bool includeRoute) =>
(includeRoute ? ctx.Request.Path.ToString() + "#" : "") + (GetRateLimitPartitionInternal(ctx) ?? "");
private static string? GetRateLimitPartitionInternal(this HttpContext ctx) =>
ctx.GetUser()?.Id ??
ctx.Request.Headers["X-Forwarded-For"].FirstOrDefault() ??
ctx.Connection.RemoteIpAddress?.ToString();

View file

@ -410,7 +410,6 @@ public class NoteService(
var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) =
resolvedMentions ?? await ResolveNoteMentionsAsync(text);
List<MfmNode>? nodes = null;
if (text != null && string.IsNullOrWhiteSpace(text))
{
@ -830,13 +829,13 @@ public class NoteService(
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage",
Justification = "Inspection doesn't understand IncludeCommonProperties()")]
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "See above")]
public async Task<Note?> ProcessNoteUpdateAsync(ASNote note, User actor)
public async Task<Note?> ProcessNoteUpdateAsync(ASNote note, User actor, User? user = null)
{
var dbNote = await db.Notes.IncludeCommonProperties()
.Include(p => p.Poll)
.FirstOrDefaultAsync(p => p.Uri == note.Id);
if (dbNote == null) return await ProcessNoteAsync(note, actor);
if (dbNote == null) return await ProcessNoteAsync(note, actor, user);
logger.LogDebug("Processing note update {id} for note {noteId}", note.Id, dbNote.Id);
@ -1027,7 +1026,7 @@ public class NoteService(
}
public async Task<Note?> ResolveNoteAsync(
string uri, ASNote? fetchedNote = null, User? user = null, bool clearHistory = false
string uri, ASNote? fetchedNote = null, User? user = null, bool clearHistory = false, bool forceRefresh = false
)
{
if (clearHistory)
@ -1051,7 +1050,7 @@ public class NoteService(
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == uri);
if (note != null) return note;
if (note != null && !forceRefresh) return note;
if (!fetchedNote?.VerifiedFetch ?? false)
fetchedNote = null;
@ -1082,7 +1081,7 @@ public class NoteService(
if (fetchedNote.Id != uri)
{
var res = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == fetchedNote.Id);
if (res != null) return res;
if (res != null && !forceRefresh) return res;
}
var actor = await userResolver.ResolveAsync(attrTo.Id);
@ -1091,7 +1090,9 @@ public class NoteService(
{
try
{
return await ProcessNoteAsync(fetchedNote, actor, user);
return forceRefresh
? await ProcessNoteUpdateAsync(fetchedNote, actor, user)
: await ProcessNoteAsync(fetchedNote, actor, user);
}
catch (Exception e)
{

View file

@ -55,4 +55,7 @@ internal class NoteControllerModel(ApiClient api)
public Task<NoteResponse> CreateNote(NoteCreateRequest request) =>
api.Call<NoteResponse>(HttpMethod.Post, "/notes", data: request);
public Task<NoteRefetchResponse?> RefetchNote(string id) =>
api.CallNullable<NoteRefetchResponse>(HttpMethod.Get, $"/notes/{id}/refetch");
}

View file

@ -0,0 +1,7 @@
namespace Iceshrimp.Shared.Schemas.Web;
public class NoteRefetchResponse
{
public required NoteResponse Note { get; set; }
public required List<string> Errors { get; set; }
}