[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);
|
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]
|
[HttpPost]
|
||||||
[Authorize("write:statuses")]
|
[Authorize("write:statuses")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
|
|
|
@ -122,6 +122,15 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
||||||
.HasPostgresEnum<UserProfile.UserProfileFFVisibility>()
|
.HasPostgresEnum<UserProfile.UserProfileFFVisibility>()
|
||||||
.HasPostgresExtension("pg_trgm");
|
.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 => {
|
modelBuilder.Entity<AbuseUserReport>(entity => {
|
||||||
entity.Property(e => e.CreatedAt).HasComment("The created date of the AbuseUserReport.");
|
entity.Property(e => e.CreatedAt).HasComment("The created date of the AbuseUserReport.");
|
||||||
entity.Property(e => e.Forwarded).HasDefaultValue(false);
|
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);
|
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.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
|
||||||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||||
|
|
Loading…
Add table
Reference in a new issue