[backend/api] Add all missing timeline endpoints
This commit is contained in:
parent
d2f1048dcc
commit
2e911805de
7 changed files with 191 additions and 9 deletions
|
@ -42,4 +42,94 @@ public class TimelineController(DatabaseContext db, NoteRenderer noteRenderer, C
|
|||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Home);
|
||||
}
|
||||
|
||||
[HttpGet("local")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetLocalTimeline(PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notes = await db.Notes.IncludeCommonProperties()
|
||||
.Where(p => p.UserHost == null)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db)
|
||||
.FilterMutedThreads(user, db)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Public);
|
||||
}
|
||||
|
||||
[HttpGet("social")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetSocialTimeline(PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var heuristic = await QueryableTimelineExtensions.GetHeuristicAsync(user, db, cache);
|
||||
var notes = await db.Notes.IncludeCommonProperties()
|
||||
.FilterByFollowingOwnAndLocal(user, db, heuristic)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db, filterHiddenListMembers: true)
|
||||
.FilterMutedThreads(user, db)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Public);
|
||||
}
|
||||
|
||||
[HttpGet("recommended")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetRecommendedTimeline(PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notes = await db.Notes.IncludeCommonProperties()
|
||||
.Where(p => db.RecommendedInstances.Any(i => i.Host == p.UserHost))
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db, filterHiddenListMembers: true)
|
||||
.FilterMutedThreads(user, db)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Public);
|
||||
}
|
||||
|
||||
[HttpGet("global")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetGlobalTimeline(PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notes = await db.Notes.IncludeCommonProperties()
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db)
|
||||
.FilterMutedThreads(user, db)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Public);
|
||||
}
|
||||
|
||||
[HttpGet("remote/{instance}")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public async Task<IEnumerable<NoteResponse>> GetRemoteTimeline(string instance, PaginationQuery pq)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
var notes = await db.Notes.IncludeCommonProperties()
|
||||
.Where(p => p.UserHost == instance)
|
||||
.EnsureVisibleFor(user)
|
||||
.FilterHidden(user, db)
|
||||
.FilterMutedThreads(user, db)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.PrecomputeVisibilities(user)
|
||||
.ToListAsync();
|
||||
|
||||
return await noteRenderer.RenderManyAsync(notes.EnforceRenoteReplyVisibility(), user,
|
||||
Filter.FilterContext.Public);
|
||||
}
|
||||
}
|
|
@ -92,6 +92,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
public virtual DbSet<Filter> Filters { get; init; } = null!;
|
||||
public virtual DbSet<PluginStoreEntry> PluginStore { get; init; } = null!;
|
||||
public virtual DbSet<PolicyConfiguration> PolicyConfiguration { get; init; } = null!;
|
||||
public virtual DbSet<RecommendedInstance> RecommendedInstances { get; init; } = null!;
|
||||
public virtual DbSet<DataProtectionKey> DataProtectionKeys { get; init; } = null!;
|
||||
|
||||
public static NpgsqlDataSource GetDataSource(Config.DatabaseSection config)
|
||||
|
|
|
@ -3651,6 +3651,18 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
b.ToTable("push_subscription");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RecommendedInstance", b =>
|
||||
{
|
||||
b.Property<string>("Host")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("host");
|
||||
|
||||
b.HasKey("Host");
|
||||
|
||||
b.ToTable("recommended_instance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Iceshrimp.Backend.Core.Database.Tables.RegistrationInvite", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20250322222922_AddRecommendedInstanceTable")]
|
||||
public partial class AddRecommendedInstanceTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "recommended_instance",
|
||||
columns: table => new
|
||||
{
|
||||
host = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_recommended_instance", x => x.host);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "recommended_instance");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Tables;
|
||||
|
||||
[Table("recommended_instance")]
|
||||
public class RecommendedInstance
|
||||
{
|
||||
[Key]
|
||||
[Column("host")]
|
||||
[StringLength(256)]
|
||||
public string Host { get; set; } = null!;
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq.Expressions;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Extensions;
|
||||
|
@ -18,16 +20,25 @@ public static class QueryableTimelineExtensions
|
|||
)
|
||||
{
|
||||
return heuristic < Cutoff
|
||||
? query.FollowingAndOwnLowFreq(user, db)
|
||||
? query.Where(FollowingAndOwnLowFreqExpr(user, db))
|
||||
: query.Where(note => note.User == user || note.User.IsFollowedBy(user));
|
||||
}
|
||||
|
||||
private static IQueryable<Note> FollowingAndOwnLowFreq(this IQueryable<Note> query, User user, DatabaseContext db)
|
||||
=> query.Where(note => db.Followings
|
||||
.Where(p => p.Follower == user)
|
||||
.Select(p => p.FolloweeId)
|
||||
.Concat(new[] { user.Id })
|
||||
.Contains(note.UserId));
|
||||
public static IQueryable<Note> FilterByFollowingOwnAndLocal(
|
||||
this IQueryable<Note> query, User user, DatabaseContext db, int heuristic
|
||||
)
|
||||
{
|
||||
return heuristic < Cutoff
|
||||
? query.Where(FollowingAndOwnLowFreqExpr(user, db).Or(p => p.UserHost == null))
|
||||
: query.Where(note => note.User == user || note.User.IsFollowedBy(user));
|
||||
}
|
||||
|
||||
private static Expression<Func<Note,bool>> FollowingAndOwnLowFreqExpr(User user, DatabaseContext db)
|
||||
=> note => db.Followings
|
||||
.Where(p => p.Follower == user)
|
||||
.Select(p => p.FolloweeId)
|
||||
.Concat(new[] { user.Id })
|
||||
.Contains(note.UserId);
|
||||
|
||||
public static IQueryable<User> NeedsTimelineHeuristicUpdate(
|
||||
this IQueryable<User> query, DatabaseContext db, TimeSpan maxRemainingTtl
|
||||
|
@ -60,7 +71,7 @@ public static class QueryableTimelineExtensions
|
|||
//TODO: maybe we should express this as a ratio between matching and non-matching posts
|
||||
return await db.Notes
|
||||
.Where(p => p.CreatedAt > latestNote.CreatedAt - TimeSpan.FromDays(7))
|
||||
.FollowingAndOwnLowFreq(user, db)
|
||||
.Where(FollowingAndOwnLowFreqExpr(user, db))
|
||||
.OrderByDescending(p => p.Id)
|
||||
.Take(Cutoff + 1)
|
||||
.CountAsync();
|
||||
|
|
|
@ -9,4 +9,24 @@ internal class TimelineControllerModel(ApiClient api)
|
|||
[LinkPagination(20, 80)]
|
||||
public Task<List<NoteResponse>> GetHomeTimelineAsync(PaginationQuery pq) =>
|
||||
api.CallAsync<List<NoteResponse>>(HttpMethod.Get, "/timelines/home", pq);
|
||||
}
|
||||
|
||||
[LinkPagination(20, 80)]
|
||||
public Task<List<NoteResponse>> GetLocalTimelineAsync(PaginationQuery pq) =>
|
||||
api.CallAsync<List<NoteResponse>>(HttpMethod.Get, "/timelines/local", pq);
|
||||
|
||||
[LinkPagination(20, 80)]
|
||||
public Task<List<NoteResponse>> GetSocialTimelineAsync(PaginationQuery pq) =>
|
||||
api.CallAsync<List<NoteResponse>>(HttpMethod.Get, "/timelines/social", pq);
|
||||
|
||||
[LinkPagination(20, 80)]
|
||||
public Task<List<NoteResponse>> GetRecommendedTimelineAsync(PaginationQuery pq) =>
|
||||
api.CallAsync<List<NoteResponse>>(HttpMethod.Get, "/timelines/recommended", pq);
|
||||
|
||||
[LinkPagination(20, 80)]
|
||||
public Task<List<NoteResponse>> GetGlobalTimelineAsync(PaginationQuery pq) =>
|
||||
api.CallAsync<List<NoteResponse>>(HttpMethod.Get, "/timelines/global", pq);
|
||||
|
||||
[LinkPagination(20, 80)]
|
||||
public Task<List<NoteResponse>> GetRemoteTimelineAsync(string instance, PaginationQuery pq) =>
|
||||
api.CallAsync<List<NoteResponse>>(HttpMethod.Get, $"/timelines/remote/{instance}", pq);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue