[backend/masto-client] Implement status context endpoint & map database functions (ISH-43)
This commit is contained in:
parent
059f059f36
commit
8745f72ea7
7 changed files with 6224 additions and 2 deletions
|
@ -0,0 +1,8 @@
|
|||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
|
||||
|
||||
public class StatusContext {
|
||||
[J("ancestors")] public required List<Status> Ancestors { get; set; }
|
||||
[J("descendants")] public required List<Status> Descendants { get; set; }
|
||||
}
|
|
@ -38,6 +38,40 @@ public class StatusController(DatabaseContext db, NoteRenderer noteRenderer, Not
|
|||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/context")]
|
||||
[Authenticate("read:statuses")]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Status))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> GetStatusContext(string id) {
|
||||
var user = HttpContext.GetUser();
|
||||
var maxAncestors = user != null ? 4096 : 40;
|
||||
var maxDescendants = user != null ? 4096 : 60;
|
||||
var maxDepth = user != null ? 4096 : 20;
|
||||
|
||||
if (!db.Notes.Any(p => p.Id == id))
|
||||
throw GracefulException.RecordNotFound();
|
||||
|
||||
var ancestors = await db.NoteAncestors(id, maxAncestors)
|
||||
.IncludeCommonProperties()
|
||||
.EnsureVisibleFor(user)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer);
|
||||
|
||||
var descendants = await db.NoteDescendants(id, maxDepth, maxDescendants)
|
||||
.IncludeCommonProperties()
|
||||
.EnsureVisibleFor(user)
|
||||
.PrecomputeVisibilities(user)
|
||||
.RenderAllForMastodonAsync(noteRenderer);
|
||||
|
||||
var res = new StatusContext {
|
||||
Ancestors = ancestors,
|
||||
Descendants = descendants
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("write:statuses")]
|
||||
[Produces("application/json")]
|
||||
|
|
|
@ -122,6 +122,15 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
.HasPostgresEnum<UserProfile.UserProfileFFVisibility>()
|
||||
.HasPostgresExtension("pg_trgm");
|
||||
|
||||
modelBuilder
|
||||
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(NoteAncestors),
|
||||
[typeof(string), typeof(int)])!)
|
||||
.HasName("note_ancestors");
|
||||
modelBuilder
|
||||
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(NoteDescendants),
|
||||
[typeof(string), typeof(int), typeof(int)])!)
|
||||
.HasName("note_descendants");
|
||||
|
||||
modelBuilder.Entity<AbuseUserReport>(entity => {
|
||||
entity.Property(e => e.CreatedAt).HasComment("The created date of the AbuseUserReport.");
|
||||
entity.Property(e => e.Forwarded).HasDefaultValue(false);
|
||||
|
@ -987,4 +996,16 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
entity.HasOne(d => d.User).WithMany(p => p.Webhooks);
|
||||
});
|
||||
}
|
||||
|
||||
public IQueryable<Note> NoteAncestors(string noteId, int depth)
|
||||
=> FromExpression(() => NoteAncestors(noteId, depth));
|
||||
|
||||
public IQueryable<Note> NoteAncestors(Note note, int depth)
|
||||
=> FromExpression(() => NoteAncestors(note.Id, depth));
|
||||
|
||||
public IQueryable<Note> NoteDescendants(string noteId, int depth, int breadth)
|
||||
=> FromExpression(() => NoteDescendants(noteId, depth, breadth));
|
||||
|
||||
public IQueryable<Note> NoteDescendants(Note note, int depth, int breadth)
|
||||
=> FromExpression(() => NoteDescendants(note.Id, depth, breadth));
|
||||
}
|
5963
Iceshrimp.Backend/Core/Database/Migrations/20240212005032_AddDatabaseFunctions.Designer.cs
generated
Normal file
5963
Iceshrimp.Backend/Core/Database/Migrations/20240212005032_AddDatabaseFunctions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,194 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDatabaseFunctions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
DROP FUNCTION public.note_ancestors;
|
||||
CREATE OR REPLACE FUNCTION public.note_ancestors (start_id character varying, max_depth integer)
|
||||
RETURNS SETOF "note"
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
"note"
|
||||
WHERE
|
||||
"id" IN( SELECT DISTINCT
|
||||
id FROM (WITH RECURSIVE ancestors AS (
|
||||
SELECT
|
||||
"id",
|
||||
"replyId" FROM "note"
|
||||
WHERE
|
||||
"id" = "start_id"
|
||||
UNION ALL
|
||||
SELECT
|
||||
"note"."id",
|
||||
"note"."replyId" FROM "note"
|
||||
JOIN ancestors ON ancestors. "replyId" = "note"."id"
|
||||
)
|
||||
SELECT
|
||||
id FROM ancestors
|
||||
LIMIT max_depth + 1) AS RECURSIVE
|
||||
WHERE
|
||||
"id" != start_id
|
||||
ORDER BY
|
||||
"id" ASC)
|
||||
$function$;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
DROP FUNCTION public.note_descendants;
|
||||
CREATE FUNCTION public.note_descendants (start_id character varying, max_depth integer, max_breadth integer)
|
||||
RETURNS SETOF "note"
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
"note"
|
||||
WHERE
|
||||
id IN( SELECT DISTINCT
|
||||
id FROM (WITH RECURSIVE tree (
|
||||
id,
|
||||
ancestors,
|
||||
depth
|
||||
) AS (
|
||||
SELECT
|
||||
start_id,
|
||||
'{}'::VARCHAR [],
|
||||
0
|
||||
UNION
|
||||
SELECT
|
||||
note.id,
|
||||
CASE WHEN note. "replyId" = tree.id THEN
|
||||
tree.ancestors || note. "replyId"
|
||||
ELSE
|
||||
tree.ancestors || note. "renoteId"
|
||||
END,
|
||||
depth + 1 FROM note,
|
||||
tree
|
||||
WHERE (note. "replyId" = tree.id
|
||||
OR(
|
||||
-- get renotes but not pure renotes
|
||||
note. "renoteId" = tree.id
|
||||
AND(note.text IS NOT NULL
|
||||
OR CARDINALITY (note. "fileIds"
|
||||
) != 0
|
||||
OR note. "hasPoll" = TRUE)))
|
||||
AND depth < max_depth)
|
||||
SELECT
|
||||
id,
|
||||
-- apply the limit per node
|
||||
row_number() OVER (PARTITION BY ancestors [array_upper(ancestors, 1)]) AS nth_child FROM tree
|
||||
WHERE
|
||||
depth > 0) AS RECURSIVE
|
||||
WHERE
|
||||
nth_child < max_breadth)
|
||||
$function$;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
DROP FUNCTION public.note_ancestors;
|
||||
CREATE FUNCTION public.note_ancestors (start_id character varying, max_depth integer)
|
||||
RETURNS TABLE (
|
||||
id character varying)
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT DISTINCT
|
||||
id
|
||||
FROM (WITH RECURSIVE ancestors AS (
|
||||
SELECT
|
||||
"id",
|
||||
"replyId"
|
||||
FROM
|
||||
"note"
|
||||
WHERE
|
||||
"id" = "start_id"
|
||||
UNION ALL
|
||||
SELECT
|
||||
"note"."id",
|
||||
"note"."replyId"
|
||||
FROM
|
||||
"note"
|
||||
JOIN ancestors ON ancestors. "replyId" = "note"."id"
|
||||
)
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
ancestors
|
||||
LIMIT max_depth + 1
|
||||
) AS RECURSIVE
|
||||
WHERE
|
||||
"id" != start_id
|
||||
ORDER BY
|
||||
"id" ASC
|
||||
$function$;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
DROP FUNCTION public.note_descendants;
|
||||
CREATE FUNCTION public.note_descendants (start_id character varying, max_depth integer, max_breadth integer)
|
||||
RETURNS TABLE (
|
||||
id character varying)
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT DISTINCT
|
||||
id
|
||||
FROM (WITH RECURSIVE tree (
|
||||
id,
|
||||
ancestors,
|
||||
depth
|
||||
) AS (
|
||||
SELECT
|
||||
start_id,
|
||||
'{}'::VARCHAR [],
|
||||
0
|
||||
UNION
|
||||
SELECT
|
||||
note.id,
|
||||
CASE WHEN note. "replyId" = tree.id THEN
|
||||
tree.ancestors || note. "replyId"
|
||||
ELSE
|
||||
tree.ancestors || note. "renoteId"
|
||||
END,
|
||||
depth + 1
|
||||
FROM
|
||||
note,
|
||||
tree
|
||||
WHERE (note. "replyId" = tree.id
|
||||
OR(
|
||||
-- get renotes but not pure renotes
|
||||
note. "renoteId" = tree.id
|
||||
AND(note.text IS NOT NULL
|
||||
OR CARDINALITY (note. "fileIds") != 0
|
||||
OR note. "hasPoll" = TRUE)))
|
||||
AND depth < max_depth
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
-- apply the limit per node
|
||||
row_number() OVER (PARTITION BY ancestors [array_upper(ancestors, 1)]) AS nth_child
|
||||
FROM
|
||||
tree
|
||||
WHERE
|
||||
depth > 0
|
||||
) AS RECURSIVE
|
||||
WHERE
|
||||
nth_child < max_breadth
|
||||
$function$;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using Iceshrimp.Backend.Controllers.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
|
|
Loading…
Add table
Reference in a new issue