[backend/masto-client] Implement status context endpoint & map database functions (ISH-43)

This commit is contained in:
Laura Hausmann 2024-02-12 01:19:44 +01:00
parent 059f059f36
commit 8745f72ea7
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
7 changed files with 6224 additions and 2 deletions

View file

@ -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; }
}

View file

@ -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")]

View file

@ -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));
}

File diff suppressed because it is too large Load diff

View file

@ -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$;
""");
}
}
}

View file

@ -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;