[backend/api] Add note refetch endpoint (ISH-352)
This commit is contained in:
parent
3316a391d5
commit
374d9d5ebf
7 changed files with 106 additions and 16 deletions
|
@ -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))]
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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");
|
||||
}
|
7
Iceshrimp.Shared/Schemas/Web/NoteRefetchResponse.cs
Normal file
7
Iceshrimp.Shared/Schemas/Web/NoteRefetchResponse.cs
Normal 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; }
|
||||
}
|
Loading…
Add table
Reference in a new issue