[backend] Apply formatting rules

This commit is contained in:
Laura Hausmann 2024-06-29 01:06:19 +02:00
parent 571f2274f2
commit df3a7bdfe5
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
82 changed files with 471 additions and 462 deletions

View file

@ -3,7 +3,6 @@ using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Federation; using Iceshrimp.Backend.Controllers.Federation;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
@ -12,6 +11,7 @@ using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -125,7 +125,7 @@ public class AdminController(
.FirstOrDefaultAsync(p => p.Id == id && p.UserHost == null); .FirstOrDefaultAsync(p => p.Id == id && p.UserHost == null);
if (note == null) return NotFound(); if (note == null) return NotFound();
var rendered = await noteRenderer.RenderAsync(note); var rendered = await noteRenderer.RenderAsync(note);
var compacted = LdHelpers.Compact(rendered); var compacted = rendered.Compact();
return Ok(compacted); return Ok(compacted);
} }

View file

@ -1,12 +1,12 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net.Mime; using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View file

@ -1,7 +1,7 @@
using System.Net; using System.Net;
using System.Net.Mime; using System.Net.Mime;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View file

@ -1,7 +1,6 @@
using System.Text; using System.Text;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Federation.Attributes; using Iceshrimp.Backend.Controllers.Federation.Attributes;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
@ -10,6 +9,7 @@ using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Queues; using Iceshrimp.Backend.Core.Queues;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;

View file

@ -4,11 +4,11 @@ using System.Text;
using System.Xml.Serialization; using System.Xml.Serialization;
using Iceshrimp.Backend.Controllers.Federation.Attributes; using Iceshrimp.Backend.Controllers.Federation.Attributes;
using Iceshrimp.Backend.Controllers.Federation.Schemas; using Iceshrimp.Backend.Controllers.Federation.Schemas;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Federation.WebFinger;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View file

@ -88,7 +88,7 @@ public class MediaController(DriveService driveSvc, DatabaseContext db) : Contro
Description = file.Comment, Description = file.Comment,
PreviewUrl = file.PublicThumbnailUrl, PreviewUrl = file.PublicThumbnailUrl,
RemoteUrl = file.Uri, RemoteUrl = file.Uri,
Sensitive = file.IsSensitive, Sensitive = file.IsSensitive
//Metadata = TODO, //Metadata = TODO,
}; };
} }

View file

@ -20,6 +20,20 @@ public class NoteRenderer(
EmojiService emojiSvc EmojiService emojiSvc
) )
{ {
private static readonly FilterResultEntity InaccessibleFilter = new()
{
Filter = new FilterEntity
{
Title = "HideInaccessible",
FilterAction = "hide",
Id = "0",
Context = ["home", "thread", "notifications", "account", "public"],
Keywords = [new FilterKeyword("RE: \ud83d\udd12", 0, 0)],
ExpiresAt = null
},
KeywordMatches = ["RE: \ud83d\udd12"] // lock emoji
};
public async Task<StatusEntity> RenderAsync( public async Task<StatusEntity> RenderAsync(
Note note, User? user, Filter.FilterContext? filterContext = null, NoteRendererDto? data = null, int recurse = 2 Note note, User? user, Filter.FilterContext? filterContext = null, NoteRendererDto? data = null, int recurse = 2
) )
@ -150,20 +164,6 @@ public class NoteRenderer(
return res; return res;
} }
private static readonly FilterResultEntity InaccessibleFilter = new()
{
Filter = new FilterEntity
{
Title = "HideInaccessible",
FilterAction = "hide",
Id = "0",
Context = ["home", "thread", "notifications", "account", "public"],
Keywords = [new FilterKeyword("RE: \ud83d\udd12", 0, 0)],
ExpiresAt = null
},
KeywordMatches = ["RE: \ud83d\udd12"] // lock emoji
};
public async Task<List<StatusEdit>> RenderHistoryAsync(Note note) public async Task<List<StatusEdit>> RenderHistoryAsync(Note note)
{ {
var edits = await db.NoteEdits.Where(p => p.Note == note).OrderBy(p => p.Id).ToListAsync(); var edits = await db.NoteEdits.Where(p => p.Note == note).OrderBy(p => p.Id).ToListAsync();
@ -430,13 +430,13 @@ public class NoteRenderer(
public List<AttachmentEntity>? Attachments; public List<AttachmentEntity>? Attachments;
public List<string>? BookmarkedNotes; public List<string>? BookmarkedNotes;
public List<EmojiEntity>? Emoji; public List<EmojiEntity>? Emoji;
public List<Filter>? Filters;
public List<string>? LikedNotes; public List<string>? LikedNotes;
public List<MentionEntity>? Mentions; public List<MentionEntity>? Mentions;
public List<string>? PinnedNotes; public List<string>? PinnedNotes;
public List<PollEntity>? Polls; public List<PollEntity>? Polls;
public List<ReactionEntity>? Reactions; public List<ReactionEntity>? Reactions;
public List<string>? Renotes; public List<string>? Renotes;
public List<Filter>? Filters;
public bool Source; public bool Source;
} }

View file

@ -9,7 +9,7 @@ namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities;
public class StatusEntity : IEntity, ICloneable public class StatusEntity : IEntity, ICloneable
{ {
[J("id")] public required string Id { get; set; } [JI] public string? MastoReplyUserId;
[J("text")] public required string? Text { get; set; } [J("text")] public required string? Text { get; set; }
[J("content")] public required string? Content { get; set; } [J("content")] public required string? Content { get; set; }
[J("uri")] public required string Uri { get; set; } [J("uri")] public required string Uri { get; set; }
@ -52,7 +52,8 @@ public class StatusEntity : IEntity, ICloneable
[J("language")] public string? Language => null; //FIXME [J("language")] public string? Language => null; //FIXME
[JI] public string? MastoReplyUserId; public object Clone() => MemberwiseClone();
[J("id")] public required string Id { get; set; }
public static string EncodeVisibility(Note.NoteVisibility visibility) public static string EncodeVisibility(Note.NoteVisibility visibility)
{ {
@ -77,8 +78,6 @@ public class StatusEntity : IEntity, ICloneable
_ => throw GracefulException.BadRequest($"Unknown visibility: {visibility}") _ => throw GracefulException.BadRequest($"Unknown visibility: {visibility}")
}; };
} }
public object Clone() => MemberwiseClone();
} }
public class StatusContext public class StatusContext

View file

@ -97,6 +97,8 @@ public abstract class StatusSchemas
public class ReblogRequest public class ReblogRequest
{ {
[B(Name = "visibility")] [J("visibility")] public string? Visibility { get; set; } [B(Name = "visibility")]
[J("visibility")]
public string? Visibility { get; set; }
} }
} }

View file

@ -12,7 +12,6 @@ using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization;
using static Iceshrimp.Parsing.MfmNodeTypes;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@ -20,6 +19,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Backend.Controllers.Mastodon; namespace Iceshrimp.Backend.Controllers.Mastodon;
@ -58,7 +58,7 @@ public class StatusController(
var note = await db.Notes var note = await db.Notes
.Where(p => p.Id == id) .Where(p => p.Id == id)
.IncludeCommonProperties() .IncludeCommonProperties()
.FilterHidden(user, db, filterOutgoingBlocks: false, filterMutes: false, .FilterHidden(user, db, false, false,
filterMentions: false) filterMentions: false)
.EnsureVisibleFor(user) .EnsureVisibleFor(user)
.PrecomputeVisibilities(user) .PrecomputeVisibilities(user)
@ -89,7 +89,7 @@ public class StatusController(
var note = await db.Notes var note = await db.Notes
.Where(p => p.Id == id) .Where(p => p.Id == id)
.EnsureVisibleFor(user) .EnsureVisibleFor(user)
.FilterHidden(user, db, filterOutgoingBlocks: false, filterMutes: false) .FilterHidden(user, db, false, false)
.FirstOrDefaultAsync() ?? .FirstOrDefaultAsync() ??
throw GracefulException.RecordNotFound(); throw GracefulException.RecordNotFound();

View file

@ -63,12 +63,6 @@ public class DirectChannel(WebSocketConnection connection) : IChannel
return wrapped; return wrapped;
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note) private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note)
{ {
var renote = note.Renote == null && rendered.Renote != null; var renote = note.Renote == null && rendered.Renote != null;
@ -149,4 +143,10 @@ public class DirectChannel(WebSocketConnection connection) : IChannel
_logger.LogError("Event handler OnNoteUpdated threw exception: {e}", e); _logger.LogError("Event handler OnNoteUpdated threw exception: {e}", e);
} }
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
} }

View file

@ -13,13 +13,13 @@ public class HashtagChannel(WebSocketConnection connection, bool local) : IChann
private readonly ILogger<HashtagChannel> _logger = private readonly ILogger<HashtagChannel> _logger =
connection.Scope.ServiceProvider.GetRequiredService<ILogger<HashtagChannel>>(); connection.Scope.ServiceProvider.GetRequiredService<ILogger<HashtagChannel>>();
private readonly WriteLockingList<string> _tags = [];
public string Name => local ? "hashtag:local" : "hashtag"; public string Name => local ? "hashtag:local" : "hashtag";
public List<string> Scopes => ["read:statuses"]; public List<string> Scopes => ["read:statuses"];
public bool IsSubscribed => _tags.Count != 0; public bool IsSubscribed => _tags.Count != 0;
public bool IsAggregate => true; public bool IsAggregate => true;
private readonly WriteLockingList<string> _tags = [];
public async Task Subscribe(StreamingRequestMessage msg) public async Task Subscribe(StreamingRequestMessage msg)
{ {
if (msg.Tag == null) if (msg.Tag == null)
@ -79,12 +79,6 @@ public class HashtagChannel(WebSocketConnection connection, bool local) : IChann
return wrapped; return wrapped;
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note) private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note)
{ {
var renote = note.Renote == null && rendered.Renote != null; var renote = note.Renote == null && rendered.Renote != null;
@ -169,4 +163,10 @@ public class HashtagChannel(WebSocketConnection connection, bool local) : IChann
_logger.LogError("Event handler OnNoteDeleted threw exception: {e}", e); _logger.LogError("Event handler OnNoteDeleted threw exception: {e}", e);
} }
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
} }

View file

@ -12,18 +12,19 @@ namespace Iceshrimp.Backend.Controllers.Mastodon.Streaming.Channels;
public class ListChannel(WebSocketConnection connection) : IChannel public class ListChannel(WebSocketConnection connection) : IChannel
{ {
private readonly WriteLockingList<string> _lists = [];
private readonly ILogger<ListChannel> _logger = private readonly ILogger<ListChannel> _logger =
connection.Scope.ServiceProvider.GetRequiredService<ILogger<ListChannel>>(); connection.Scope.ServiceProvider.GetRequiredService<ILogger<ListChannel>>();
private readonly ConcurrentDictionary<string, WriteLockingList<string>> _members = [];
private IEnumerable<string> _applicableUserIds = [];
public string Name => "list"; public string Name => "list";
public List<string> Scopes => ["read:statuses"]; public List<string> Scopes => ["read:statuses"];
public bool IsSubscribed => _lists.Count != 0; public bool IsSubscribed => _lists.Count != 0;
public bool IsAggregate => true; public bool IsAggregate => true;
private readonly WriteLockingList<string> _lists = [];
private readonly ConcurrentDictionary<string, WriteLockingList<string>> _members = [];
private IEnumerable<string> _applicableUserIds = [];
public async Task Subscribe(StreamingRequestMessage msg) public async Task Subscribe(StreamingRequestMessage msg)
{ {
if (msg.List == null) if (msg.List == null)
@ -101,12 +102,6 @@ public class ListChannel(WebSocketConnection connection) : IChannel
return wrapped; return wrapped;
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note) private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note)
{ {
var renote = note.Renote == null && rendered.Renote != null; var renote = note.Renote == null && rendered.Renote != null;
@ -218,4 +213,10 @@ public class ListChannel(WebSocketConnection connection) : IChannel
_logger.LogError("Event handler OnListUpdated threw exception: {e}", e); _logger.LogError("Event handler OnListUpdated threw exception: {e}", e);
} }
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
} }

View file

@ -71,12 +71,6 @@ public class PublicChannel(
return wrapped; return wrapped;
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note) private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note)
{ {
var renote = note.Renote == null && rendered.Renote != null; var renote = note.Renote == null && rendered.Renote != null;
@ -160,4 +154,10 @@ public class PublicChannel(
_logger.LogError("Event handler OnNoteDeleted threw exception: {e}", e); _logger.LogError("Event handler OnNoteDeleted threw exception: {e}", e);
} }
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
} }

View file

@ -83,12 +83,6 @@ public class UserChannel(WebSocketConnection connection, bool notificationsOnly)
return wrapped; return wrapped;
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note) private static StatusEntity EnforceRenoteReplyVisibility(StatusEntity rendered, NoteWithVisibilities note)
{ {
var renote = note.Renote == null && rendered.Renote != null; var renote = note.Renote == null && rendered.Renote != null;
@ -206,4 +200,10 @@ public class UserChannel(WebSocketConnection connection, bool notificationsOnly)
_logger.LogError("Event handler OnNotification threw exception: {e}", e); _logger.LogError("Event handler OnNotification threw exception: {e}", e);
} }
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
}
} }

View file

@ -23,18 +23,18 @@ public sealed class WebSocketConnection(
CancellationToken ct CancellationToken ct
) : IDisposable ) : IDisposable
{ {
private readonly WriteLockingList<string> _blockedBy = [];
private readonly WriteLockingList<string> _blocking = [];
private readonly SemaphoreSlim _lock = new(1); private readonly SemaphoreSlim _lock = new(1);
private readonly WriteLockingList<string> _muting = [];
public readonly List<IChannel> Channels = []; public readonly List<IChannel> Channels = [];
public readonly EventService EventService = eventSvc; public readonly EventService EventService = eventSvc;
public readonly WriteLockingList<Filter> Filters = [];
public readonly WriteLockingList<string> Following = [];
public readonly IServiceScope Scope = scopeFactory.CreateScope(); public readonly IServiceScope Scope = scopeFactory.CreateScope();
public readonly IServiceScopeFactory ScopeFactory = scopeFactory; public readonly IServiceScopeFactory ScopeFactory = scopeFactory;
public readonly OauthToken Token = token; public readonly OauthToken Token = token;
public List<string> HiddenFromHome = []; public List<string> HiddenFromHome = [];
public readonly WriteLockingList<Filter> Filters = [];
public readonly WriteLockingList<string> Following = [];
private readonly WriteLockingList<string> _blocking = [];
private readonly WriteLockingList<string> _blockedBy = [];
private readonly WriteLockingList<string> _muting = [];
public void Dispose() public void Dispose()
{ {
@ -362,9 +362,9 @@ public sealed class WebSocketConnection(
(note.Renote?.User != null && (note.Renote?.User != null &&
(IsFiltered(note.Renote.User) || (IsFiltered(note.Renote.User) ||
IsFilteredMentions(note.Renote.Mentions))) || IsFilteredMentions(note.Renote.Mentions))) ||
note.Renote?.Renote?.User != null && (note.Renote?.Renote?.User != null &&
(IsFiltered(note.Renote.Renote.User) || (IsFiltered(note.Renote.Renote.User) ||
IsFilteredMentions(note.Renote.Renote.Mentions)); IsFilteredMentions(note.Renote.Renote.Mentions)));
public async Task CloseAsync(WebSocketCloseStatus status) public async Task CloseAsync(WebSocketCloseStatus status)
{ {

View file

@ -4,13 +4,13 @@ using System.Net.Mime;
using AsyncKeyedLock; using AsyncKeyedLock;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -45,7 +45,7 @@ public class NoteController(
var note = await db.Notes.Where(p => p.Id == id) var note = await db.Notes.Where(p => p.Id == id)
.IncludeCommonProperties() .IncludeCommonProperties()
.EnsureVisibleFor(user) .EnsureVisibleFor(user)
.FilterHidden(user, db, filterOutgoingBlocks: false, filterMutes: false) .FilterHidden(user, db, false, false)
.PrecomputeVisibilities(user) .PrecomputeVisibilities(user)
.FirstOrDefaultAsync() ?? .FirstOrDefaultAsync() ??
throw GracefulException.NotFound("Note not found"); throw GracefulException.NotFound("Note not found");
@ -65,7 +65,7 @@ public class NoteController(
var note = await db.Notes.Where(p => p.Id == id) var note = await db.Notes.Where(p => p.Id == id)
.EnsureVisibleFor(user) .EnsureVisibleFor(user)
.FilterHidden(user, db, filterOutgoingBlocks: false, filterMutes: false) .FilterHidden(user, db, false, false)
.FirstOrDefaultAsync() ?? .FirstOrDefaultAsync() ??
throw GracefulException.NotFound("Note not found"); throw GracefulException.NotFound("Note not found");
@ -94,7 +94,7 @@ public class NoteController(
var note = await db.Notes.Where(p => p.Id == id) var note = await db.Notes.Where(p => p.Id == id)
.EnsureVisibleFor(user) .EnsureVisibleFor(user)
.FilterHidden(user, db, filterOutgoingBlocks: false, filterMutes: false) .FilterHidden(user, db, false, false)
.FirstOrDefaultAsync() ?? .FirstOrDefaultAsync() ??
throw GracefulException.NotFound("Note not found"); throw GracefulException.NotFound("Note not found");

View file

@ -2,10 +2,10 @@ using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Controllers.Schemas;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View file

@ -1,10 +1,10 @@
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -219,10 +219,10 @@ public class NoteRenderer(
public class NoteRendererDto public class NoteRendererDto
{ {
public List<NoteAttachment>? Attachments; public List<NoteAttachment>? Attachments;
public List<NoteReactionSchema>? Reactions; public List<EmojiResponse>? Emoji;
public List<UserResponse>? Users;
public List<Filter>? Filters; public List<Filter>? Filters;
public List<string>? LikedNotes; public List<string>? LikedNotes;
public List<EmojiResponse>? Emoji; public List<NoteReactionSchema>? Reactions;
public List<UserResponse>? Users;
} }
} }

View file

@ -1,6 +1,6 @@
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Shared.Schemas;
namespace Iceshrimp.Backend.Controllers.Renderers; namespace Iceshrimp.Backend.Controllers.Renderers;

View file

@ -1,7 +1,7 @@
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Shared.Schemas;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Renderers; namespace Iceshrimp.Backend.Controllers.Renderers;
@ -91,14 +91,14 @@ public class UserProfileRenderer(DatabaseContext db)
public class RelationData public class RelationData
{ {
public required string UserId; public required bool IsBlocking;
public required bool IsSelf;
public required bool IsFollowing;
public required bool IsFollowedBy; public required bool IsFollowedBy;
public required bool IsFollowing;
public required bool IsMuting;
public required bool IsRequested; public required bool IsRequested;
public required bool IsRequestedBy; public required bool IsRequestedBy;
public required bool IsBlocking; public required bool IsSelf;
public required bool IsMuting; public required string UserId;
public static implicit operator Relations(RelationData data) public static implicit operator Relations(RelationData data)
{ {

View file

@ -1,8 +1,8 @@
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Shared.Schemas;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;

View file

@ -2,12 +2,12 @@ using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Controllers.Schemas;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View file

@ -2,12 +2,12 @@ using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Controllers.Schemas;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View file

@ -17,10 +17,10 @@ public sealed class Config
public sealed class InstanceSection public sealed class InstanceSection
{ {
public readonly string Version;
public readonly string Codename; public readonly string Codename;
public readonly string? CommitHash; public readonly string? CommitHash;
public readonly string RawVersion; public readonly string RawVersion;
public readonly string Version;
public InstanceSection() public InstanceSection()
{ {
@ -100,9 +100,9 @@ public sealed class Config
public sealed class StorageSection public sealed class StorageSection
{ {
public readonly TimeSpan? MediaRetentionTimeSpan;
public readonly int? MaxCacheSizeBytes; public readonly int? MaxCacheSizeBytes;
public readonly int? MaxUploadSizeBytes; public readonly int? MaxUploadSizeBytes;
public readonly TimeSpan? MediaRetentionTimeSpan;
public bool CleanAvatars = false; public bool CleanAvatars = false;
public bool CleanBanners = false; public bool CleanBanners = false;

View file

@ -14,6 +14,13 @@ public static class Enums
ObjectStorage = 1 ObjectStorage = 1
} }
public enum ImageProcessor
{
ImageSharp,
LibVips,
None
}
public enum ItemVisibility public enum ItemVisibility
{ {
Hide = 0, Hide = 0,
@ -21,13 +28,6 @@ public static class Enums
Public = 2 Public = 2
} }
public enum Registrations
{
Closed = 0,
Invite = 1,
Open = 2
}
public enum PublicPreview public enum PublicPreview
{ {
Lockdown = 0, Lockdown = 0,
@ -36,10 +36,10 @@ public static class Enums
Public = 3 Public = 3
} }
public enum ImageProcessor public enum Registrations
{ {
ImageSharp, Closed = 0,
LibVips, Invite = 1,
None Open = 2
} }
} }

View file

@ -7,6 +7,6 @@ public interface IEntity
public class EntityWrapper<T> : IEntity public class EntityWrapper<T> : IEntity
{ {
public required string Id { get; init; }
public required T Entity { get; init; } public required T Entity { get; init; }
public required string Id { get; init; }
} }

View file

@ -10,6 +10,13 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
[Index("user_id")] [Index("user_id")]
public class Filter public class Filter
{ {
[PgName("filter_action_enum")]
public enum FilterAction
{
[PgName("warn")] Warn,
[PgName("hide")] Hide
}
[PgName("filter_context_enum")] [PgName("filter_context_enum")]
public enum FilterContext public enum FilterContext
{ {
@ -18,17 +25,11 @@ public class Filter
[PgName("threads")] Threads, [PgName("threads")] Threads,
[PgName("notifications")] Notifications, [PgName("notifications")] Notifications,
[PgName("accounts")] Accounts, [PgName("accounts")] Accounts,
[PgName("public")] Public, [PgName("public")] Public
} }
[PgName("filter_action_enum")] [Key]
public enum FilterAction [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
{
[PgName("warn")] Warn,
[PgName("hide")] Hide,
}
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("id")] [Column("id")]
public long Id { get; set; } public long Id { get; set; }

View file

@ -37,8 +37,7 @@ public class FollowRequest : IEntity
[StringLength(128)] [StringLength(128)]
public string? RequestId { get; set; } public string? RequestId { get; set; }
[Column("relationshipId")] [Column("relationshipId")] public Guid? RelationshipId { get; set; }
public Guid? RelationshipId { get; set; }
/// <summary> /// <summary>
/// [Denormalized] /// [Denormalized]

View file

@ -80,8 +80,7 @@ public class Following
[StringLength(512)] [StringLength(512)]
public string? FolloweeSharedInbox { get; set; } public string? FolloweeSharedInbox { get; set; }
[Column("relationshipId")] [Column("relationshipId")] public Guid? RelationshipId { get; set; }
public Guid? RelationshipId { get; set; }
[ForeignKey(nameof(FolloweeId))] [ForeignKey(nameof(FolloweeId))]
[InverseProperty(nameof(User.IncomingFollowRelationships))] [InverseProperty(nameof(User.IncomingFollowRelationships))]

View file

@ -155,7 +155,8 @@ public class Note : IEntity
public string? ReplyUserId { get; set; } public string? ReplyUserId { get; set; }
/// <summary> /// <summary>
/// Mastodon requires a slightly differently computed replyUserId field. To save processing time, we do this ahead of time. /// Mastodon requires a slightly differently computed replyUserId field. To save processing time, we do this ahead of
/// time.
/// </summary> /// </summary>
[Column("mastoReplyUserId")] [Column("mastoReplyUserId")]
[StringLength(32)] [StringLength(32)]

View file

@ -266,8 +266,7 @@ public class User : IEntity
[StringLength(128)] [StringLength(128)]
public string? BannerBlurhash { get; set; } public string? BannerBlurhash { get; set; }
[Column("splitDomainResolved")] [Column("splitDomainResolved")] public bool SplitDomainResolved { get; set; }
public bool SplitDomainResolved { get; set; }
[InverseProperty(nameof(AbuseUserReport.Assignee))] [InverseProperty(nameof(AbuseUserReport.Assignee))]
public virtual ICollection<AbuseUserReport> AbuseUserReportAssignees { get; set; } = new List<AbuseUserReport>(); public virtual ICollection<AbuseUserReport> AbuseUserReportAssignees { get; set; } = new List<AbuseUserReport>();

View file

@ -1,7 +1,6 @@
using System.Buffers; using System.Buffers;
using System.Net; using System.Net;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json;
using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -9,6 +8,8 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Iceshrimp.Backend.Core.Extensions; namespace Iceshrimp.Backend.Core.Extensions;
@ -42,9 +43,9 @@ public static class MvcBuilderExtensions
public static IMvcBuilder ConfigureNewtonsoftJson(this IMvcBuilder builder) public static IMvcBuilder ConfigureNewtonsoftJson(this IMvcBuilder builder)
{ {
Newtonsoft.Json.JsonConvert.DefaultSettings = () => new Newtonsoft.Json.JsonSerializerSettings JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{ {
DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc DateTimeZoneHandling = DateTimeZoneHandling.Utc
}; };
return builder; return builder;

View file

@ -4,7 +4,6 @@ using System.Xml.Linq;
using Iceshrimp.Backend.Controllers.Federation; using Iceshrimp.Backend.Controllers.Federation;
using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Federation.WebFinger;
@ -13,6 +12,7 @@ using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Backend.Hubs.Authentication; using Iceshrimp.Backend.Hubs.Authentication;
using Iceshrimp.Shared.Configuration; using Iceshrimp.Shared.Configuration;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
@ -20,6 +20,7 @@ using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationM
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -120,7 +121,7 @@ public static class ServiceExtensions
.ConfigureWithValidation<Config.LocalStorageSection>(configuration, "Storage:Local") .ConfigureWithValidation<Config.LocalStorageSection>(configuration, "Storage:Local")
.ConfigureWithValidation<Config.ObjectStorageSection>(configuration, "Storage:ObjectStorage"); .ConfigureWithValidation<Config.ObjectStorageSection>(configuration, "Storage:ObjectStorage");
services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options => services.Configure<JsonOptions>(options =>
{ {
options.SerializerOptions.PropertyNamingPolicy = JsonSerialization.Options.PropertyNamingPolicy; options.SerializerOptions.PropertyNamingPolicy = JsonSerialization.Options.PropertyNamingPolicy;
foreach (var converter in JsonSerialization.Options.Converters) foreach (var converter in JsonSerialization.Options.Converters)
@ -324,7 +325,8 @@ public static class HttpContextExtensions
/// <summary> /// <summary>
/// Async equivalent of EntityFrameworkCoreDataProtectionExtensions.PersistKeysToDbContext. /// Async equivalent of EntityFrameworkCoreDataProtectionExtensions.PersistKeysToDbContext.
/// Required because Npgsql doesn't support the non-async APIs when using connection multiplexing, and the stock version EFCore API calls their blocking equivalents. /// Required because Npgsql doesn't support the non-async APIs when using connection multiplexing, and the stock
/// version EFCore API calls their blocking equivalents.
/// </summary> /// </summary>
file static class DataProtectionExtensions file static class DataProtectionExtensions
{ {

View file

@ -23,22 +23,6 @@ public static class SwaggerGenOptionsExtensions
options.DocInclusionPredicate(DocInclusionPredicate); options.DocInclusionPredicate(DocInclusionPredicate);
} }
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
Justification = "SwaggerGenOptions.SchemaFilter<T> instantiates this class at runtime")]
private class RequireNonNullablePropertiesSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
var additionalRequiredProps = model.Properties
.Where(x => !x.Value.Nullable && !model.Required.Contains(x.Key))
.Select(x => x.Key);
foreach (var propKey in additionalRequiredProps)
{
model.Required.Add(propKey);
}
}
}
private static bool DocInclusionPredicate(string docName, ApiDescription apiDesc) private static bool DocInclusionPredicate(string docName, ApiDescription apiDesc)
{ {
if (!apiDesc.TryGetMethodInfo(out var methodInfo)) return false; if (!apiDesc.TryGetMethodInfo(out var methodInfo)) return false;
@ -61,6 +45,22 @@ public static class SwaggerGenOptionsExtensions
}; };
} }
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
Justification = "SwaggerGenOptions.SchemaFilter<T> instantiates this class at runtime")]
private class RequireNonNullablePropertiesSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
var additionalRequiredProps = model.Properties
.Where(x => !x.Value.Nullable && !model.Required.Contains(x.Key))
.Select(x => x.Key);
foreach (var propKey in additionalRequiredProps)
{
model.Required.Add(propKey);
}
}
}
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")] Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")]
private class AuthorizeCheckOperationFilter : IOperationFilter private class AuthorizeCheckOperationFilter : IOperationFilter

View file

@ -4,11 +4,11 @@ using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Migrations; using Iceshrimp.Backend.Core.Database.Migrations;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.WebPush;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration.Ini; using Microsoft.Extensions.Configuration.Ini;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Iceshrimp.WebPush;
namespace Iceshrimp.Backend.Core.Extensions; namespace Iceshrimp.Backend.Core.Extensions;

View file

@ -3,9 +3,9 @@ using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization;
using static Iceshrimp.Parsing.MfmNodeTypes;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.FSharp.Collections; using Microsoft.FSharp.Collections;
using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Backend.Core.Federation.ActivityPub; namespace Iceshrimp.Backend.Core.Federation.ActivityPub;

View file

@ -18,14 +18,6 @@ public static class LdHelpers
private static readonly string Prefix = Path.Combine(AssemblyLocation, "contexts"); private static readonly string Prefix = Path.Combine(AssemblyLocation, "contexts");
private static JToken GetPreloadedDocument(string filename) =>
JToken.Parse(File.ReadAllText(Path.Combine(Prefix, filename)));
private static RemoteDocument GetPreloadedContext(string filename) => new()
{
Document = GetPreloadedDocument(filename)
};
private static readonly Dictionary<string, RemoteDocument> PreloadedContexts = new() private static readonly Dictionary<string, RemoteDocument> PreloadedContexts = new()
{ {
{ "https://www.w3.org/ns/activitystreams", GetPreloadedContext("as.json") }, { "https://www.w3.org/ns/activitystreams", GetPreloadedContext("as.json") },
@ -66,6 +58,14 @@ public static class LdHelpers
private static IEnumerable<string> ASForceArray => ["tag", "attachment", "to", "cc", "bcc", "bto"]; private static IEnumerable<string> ASForceArray => ["tag", "attachment", "to", "cc", "bcc", "bto"];
private static JToken GetPreloadedDocument(string filename) =>
JToken.Parse(File.ReadAllText(Path.Combine(Prefix, filename)));
private static RemoteDocument GetPreloadedContext(string filename) => new()
{
Document = GetPreloadedDocument(filename)
};
private static RemoteDocument CustomLoader(Uri uri, JsonLdLoaderOptions jsonLdLoaderOptions) private static RemoteDocument CustomLoader(Uri uri, JsonLdLoaderOptions jsonLdLoaderOptions)
{ {
var key = uri.AbsolutePath == "/schemas/litepub-0.1.jsonld" ? "litepub-0.1" : uri.ToString(); var key = uri.AbsolutePath == "/schemas/litepub-0.1.jsonld" ? "litepub-0.1" : uri.ToString();

View file

@ -9,11 +9,10 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
public class ASNote : ASObject public class ASNote : ASObject
{ {
public ASNote(bool withType = true) => Type = withType ? Types.Note : null; private string? _mkContent;
[JI] public bool VerifiedFetch = false; [JI] public bool VerifiedFetch = false;
public ASNote(bool withType = true) => Type = withType ? Types.Note : null;
private string? _mkContent;
[J("https://misskey-hub.net/ns#_misskey_content")] [J("https://misskey-hub.net/ns#_misskey_content")]
[JC(typeof(VC))] [JC(typeof(VC))]

View file

@ -184,11 +184,11 @@ public static class HttpSignature
) )
{ {
public readonly string Algo = algo; public readonly string Algo = algo;
public readonly IEnumerable<string> Headers = headers;
public readonly string KeyId = keyId;
public readonly byte[] Signature = signature;
public readonly string? Created = created; public readonly string? Created = created;
public readonly string? Expires = expires; public readonly string? Expires = expires;
public readonly IEnumerable<string> Headers = headers;
public readonly string KeyId = keyId;
public readonly string? Opaque = opaque; public readonly string? Opaque = opaque;
public readonly byte[] Signature = signature;
} }
} }

View file

@ -4,12 +4,6 @@ namespace Iceshrimp.Backend.Core.Helpers;
public static class CryptographyHelpers public static class CryptographyHelpers
{ {
public static string GenerateRandomHexString(int length)
=> RandomNumberGenerator.GetHexString(length, true);
public static string GenerateRandomString(int length, Charset charset = Charset.AlphaNum)
=> RandomNumberGenerator.GetString(GetCharset(charset), length);
public enum Charset public enum Charset
{ {
AlphaNum, AlphaNum,
@ -18,6 +12,17 @@ public static class CryptographyHelpers
CrockfordBase32Lower CrockfordBase32Lower
} }
private const string AlphaNumCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private const string AlphaNumLowerCharset = "abcdefghijklmnopqrstuvwxyz0123456789";
private const string CrockfordBase32Charset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
private const string CrockfordBase32LowerCharset = "0123456789abcdefghjkmnpqrstvwxyz";
public static string GenerateRandomHexString(int length)
=> RandomNumberGenerator.GetHexString(length, true);
public static string GenerateRandomString(int length, Charset charset = Charset.AlphaNum)
=> RandomNumberGenerator.GetString(GetCharset(charset), length);
private static string GetCharset(Charset charset) => charset switch private static string GetCharset(Charset charset) => charset switch
{ {
Charset.AlphaNum => AlphaNumCharset, Charset.AlphaNum => AlphaNumCharset,
@ -26,9 +31,4 @@ public static class CryptographyHelpers
Charset.CrockfordBase32Lower => CrockfordBase32LowerCharset, Charset.CrockfordBase32Lower => CrockfordBase32LowerCharset,
_ => throw new ArgumentOutOfRangeException(nameof(charset), charset, null) _ => throw new ArgumentOutOfRangeException(nameof(charset), charset, null)
}; };
private const string AlphaNumCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private const string AlphaNumLowerCharset = "abcdefghijklmnopqrstuvwxyz0123456789";
private const string CrockfordBase32Charset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
private const string CrockfordBase32LowerCharset = "0123456789abcdefghjkmnpqrstvwxyz";
} }

View file

@ -2,8 +2,8 @@ namespace Iceshrimp.Backend.Core.Helpers;
public sealed class AsyncAutoResetEvent(bool signaled = false) public sealed class AsyncAutoResetEvent(bool signaled = false)
{ {
private readonly List<TaskCompletionSource<bool>> _taskCompletionSources = [];
private readonly List<TaskCompletionSource<bool>> _noResetTaskCompletionSources = []; private readonly List<TaskCompletionSource<bool>> _noResetTaskCompletionSources = [];
private readonly List<TaskCompletionSource<bool>> _taskCompletionSources = [];
public bool Signaled => signaled; public bool Signaled => signaled;
public Task<bool> WaitAsync(CancellationToken cancellationToken = default) public Task<bool> WaitAsync(CancellationToken cancellationToken = default)

View file

@ -77,7 +77,7 @@ public static class FilterHelper
return keyword; return keyword;
} }
else if ((note.Text != null && note.Text.Contains(keyword, StringComparison.InvariantCultureIgnoreCase)) || else if ((note.Text != null && note.Text.Contains(keyword, StringComparison.InvariantCultureIgnoreCase)) ||
note.Cw != null && note.Cw.Contains(keyword, StringComparison.InvariantCultureIgnoreCase)) (note.Cw != null && note.Cw.Contains(keyword, StringComparison.InvariantCultureIgnoreCase)))
{ {
return keyword; return keyword;
} }

View file

@ -8,9 +8,9 @@ using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using static Iceshrimp.Parsing.MfmNodeTypes;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.FSharp.Collections; using Microsoft.FSharp.Collections;
using static Iceshrimp.Parsing.MfmNodeTypes;
using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser; using MfmHtmlParser = Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing.HtmlParser;
using HtmlParser = AngleSharp.Html.Parser.HtmlParser; using HtmlParser = AngleSharp.Html.Parser.HtmlParser;

View file

@ -6,13 +6,6 @@ namespace Iceshrimp.Backend.Core.Helpers;
public static class NoteThreadHelpers public static class NoteThreadHelpers
{ {
public class TreeNode<T>(T self)
{
public readonly T Self = self;
public List<TreeNode<T>> Descendants = [];
public TreeNode<T>? Parent;
}
public static List<NoteResponse> OrderAncestors(this List<NoteResponse> notes) public static List<NoteResponse> OrderAncestors(this List<NoteResponse> notes)
{ {
var final = new List<NoteResponse>(); var final = new List<NoteResponse>();
@ -139,4 +132,11 @@ public static class NoteThreadHelpers
return nodes; return nodes;
} }
public class TreeNode<T>(T self)
{
public readonly T Self = self;
public List<TreeNode<T>> Descendants = [];
public TreeNode<T>? Parent;
}
} }

View file

@ -17,6 +17,23 @@ public class WriteLockingList<T>(IEnumerable<T>? sourceCollection = null) : ICol
lock (_list) _list.Add(item); lock (_list) _list.Add(item);
} }
public void Clear()
{
lock (_list) _list.Clear();
}
public bool Contains(T item) => _list.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
public bool Remove(T item)
{
lock (_list) return _list.Remove(item);
}
public int Count => _list.Count;
public bool IsReadOnly => ((ICollection<T>)_list).IsReadOnly;
public bool AddIfMissing(T item) public bool AddIfMissing(T item)
{ {
lock (_list) lock (_list)
@ -32,25 +49,8 @@ public class WriteLockingList<T>(IEnumerable<T>? sourceCollection = null) : ICol
lock (_list) _list.AddRange(item); lock (_list) _list.AddRange(item);
} }
public void Clear()
{
lock (_list) _list.Clear();
}
public bool Contains(T item) => _list.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
public bool Remove(T item)
{
lock (_list) return _list.Remove(item);
}
public int RemoveAll(Predicate<T> predicate) public int RemoveAll(Predicate<T> predicate)
{ {
lock (_list) return _list.RemoveAll(predicate); lock (_list) return _list.RemoveAll(predicate);
} }
public int Count => _list.Count;
public bool IsReadOnly => ((ICollection<T>)_list).IsReadOnly;
} }

View file

@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Attributes;
using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Shared.Schemas;
using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Shared.Schemas;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Middleware; namespace Iceshrimp.Backend.Core.Middleware;

View file

@ -70,7 +70,7 @@ public class BackgroundTaskQueue(int parallelism)
await db.DriveFiles.AnyAsync(p => p.Id != file.Id && await db.DriveFiles.AnyAsync(p => p.Id != file.Id &&
p.AccessKey == file.AccessKey && p.AccessKey == file.AccessKey &&
!p.IsLink, !p.IsLink,
cancellationToken: token); token);
if (!deduplicated) if (!deduplicated)
{ {
@ -128,7 +128,7 @@ public class BackgroundTaskQueue(int parallelism)
if (file.AccessKey == null) return; if (file.AccessKey == null) return;
var deduplicated = var deduplicated =
await db.DriveFiles.AnyAsync(p => p.Id != file.Id && p.AccessKey == file.AccessKey && !p.IsLink, await db.DriveFiles.AnyAsync(p => p.Id != file.Id && p.AccessKey == file.AccessKey && !p.IsLink,
cancellationToken: token); token);
if (deduplicated) if (deduplicated)
return; return;
@ -267,7 +267,7 @@ public class BackgroundTaskQueue(int parallelism)
var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(user); var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(user);
await bgDb.Instances.Where(p => p.Id == dbInstance.Id) await bgDb.Instances.Where(p => p.Id == dbInstance.Id)
.ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount - 1), .ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount - 1),
cancellationToken: token); token);
}); });
logger.LogDebug("User {id} deleted successfully", jobData.UserId); logger.LogDebug("User {id} deleted successfully", jobData.UserId);

View file

@ -208,15 +208,15 @@ public class CustomHttpClient : HttpClient
private class RedirectHandler : DelegatingHandler private class RedirectHandler : DelegatingHandler
{ {
private int MaxAutomaticRedirections { get; set; }
private bool InitialAutoRedirect { get; set; }
public RedirectHandler(HttpMessageHandler innerHandler) : base(innerHandler) public RedirectHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{ {
var mostInnerHandler = innerHandler.GetMostInnerHandler(); var mostInnerHandler = innerHandler.GetMostInnerHandler();
SetupCustomAutoRedirect(mostInnerHandler); SetupCustomAutoRedirect(mostInnerHandler);
} }
private int MaxAutomaticRedirections { get; set; }
private bool InitialAutoRedirect { get; set; }
private void SetupCustomAutoRedirect(HttpMessageHandler? mostInnerHandler) private void SetupCustomAutoRedirect(HttpMessageHandler? mostInnerHandler)
{ {
//Store the initial auto-redirect & max-auto-redirect values. //Store the initial auto-redirect & max-auto-redirect values.

View file

@ -74,10 +74,10 @@ public class ImageProcessor
public class Result public class Result
{ {
public string? Blurhash; public string? Blurhash;
public Func<Stream, Task>? RenderThumbnail;
public Func<Stream, Task>? RenderWebpublic;
public required DriveFile.FileProperties Properties; public required DriveFile.FileProperties Properties;
public Func<Stream, Task>? RenderThumbnail;
public Func<Stream, Task>? RenderWebpublic;
} }
public async Task<Result?> ProcessImage(Stream data, DriveFileCreationRequest request, bool genThumb, bool genWebp) public async Task<Result?> ProcessImage(Stream data, DriveFileCreationRequest request, bool genThumb, bool genWebp)

View file

@ -65,7 +65,7 @@ public class MetaService([FromKeyedServices("cache")] DatabaseContext db)
{ {
await db.MetaStore.Upsert(new MetaStoreEntry { Key = key, Value = value }) await db.MetaStore.Upsert(new MetaStoreEntry { Key = key, Value = value })
.On(p => p.Key) .On(p => p.Key)
.WhenMatched((_, orig) => new MetaStoreEntry { Value = orig.Value, }) .WhenMatched((_, orig) => new MetaStoreEntry { Value = orig.Value })
.RunAsync(); .RunAsync();
} }
} }

View file

@ -9,11 +9,11 @@ using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization;
using static Iceshrimp.Parsing.MfmNodeTypes;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Queues; using Iceshrimp.Backend.Core.Queues;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;
@ -49,8 +49,6 @@ public class NoteService(
) )
{ {
private const int DefaultRecursionLimit = 100; private const int DefaultRecursionLimit = 100;
private readonly List<string> _resolverHistory = [];
private int _recursionLimit = DefaultRecursionLimit;
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o => private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{ {
@ -58,6 +56,9 @@ public class NoteService(
o.PoolInitialFill = 5; o.PoolInitialFill = 5;
}); });
private readonly List<string> _resolverHistory = [];
private int _recursionLimit = DefaultRecursionLimit;
public async Task<Note> CreateNoteAsync( public async Task<Note> CreateNoteAsync(
User user, Note.NoteVisibility visibility, string? text = null, string? cw = null, Note? reply = null, User user, Note.NoteVisibility visibility, string? text = null, string? cw = null, Note? reply = null,
Note? renote = null, IReadOnlyCollection<DriveFile>? attachments = null, Poll? poll = null, Note? renote = null, IReadOnlyCollection<DriveFile>? attachments = null, Poll? poll = null,
@ -632,7 +633,8 @@ public class NoteService(
note.Visibility, note.Visibility,
note.User.GetPublicUri(config.Value) + note.User.GetPublicUri(config.Value) +
"/followers")) "/followers"))
: ActivityPub.ActivityRenderer.RenderDelete(actor, new ASTombstone { Id = note.GetPublicUri(config.Value) }); : ActivityPub.ActivityRenderer.RenderDelete(actor,
new ASTombstone { Id = note.GetPublicUri(config.Value) });
if (note.Visibility == Note.NoteVisibility.Specified) if (note.Visibility == Note.NoteVisibility.Specified)
await deliverSvc.DeliverToAsync(activity, note.User, recipients.ToArray()); await deliverSvc.DeliverToAsync(activity, note.User, recipients.ToArray());

View file

@ -14,14 +14,14 @@ public class ObjectStorageService(IOptions<Config.StorageSection> config, HttpCl
{ {
private readonly string? _accessUrl = config.Value.ObjectStorage?.AccessUrl; private readonly string? _accessUrl = config.Value.ObjectStorage?.AccessUrl;
private readonly S3Bucket? _bucket = GetBucketSafely(config);
private readonly string? _prefix = config.Value.ObjectStorage?.Prefix?.Trim('/');
private readonly IReadOnlyDictionary<string, string>? _acl = config.Value.ObjectStorage?.SetAcl != null private readonly IReadOnlyDictionary<string, string>? _acl = config.Value.ObjectStorage?.SetAcl != null
? new Dictionary<string, string> { { "x-amz-acl", config.Value.ObjectStorage.SetAcl } }.AsReadOnly() ? new Dictionary<string, string> { { "x-amz-acl", config.Value.ObjectStorage.SetAcl } }.AsReadOnly()
: null; : null;
private readonly S3Bucket? _bucket = GetBucketSafely(config);
private readonly string? _prefix = config.Value.ObjectStorage?.Prefix?.Trim('/');
private static S3Bucket? GetBucketSafely(IOptions<Config.StorageSection> config) private static S3Bucket? GetBucketSafely(IOptions<Config.StorageSection> config)
{ {
if (config.Value.Provider != Enums.FileStorage.Local) return GetBucket(config); if (config.Value.Provider != Enums.FileStorage.Local) return GetBucket(config);

View file

@ -8,9 +8,9 @@ using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.WebPush;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Iceshrimp.WebPush;
using PushSubscription = Iceshrimp.Backend.Core.Database.Tables.PushSubscription; using PushSubscription = Iceshrimp.Backend.Core.Database.Tables.PushSubscription;
using WebPushSubscription = Iceshrimp.WebPush.PushSubscription; using WebPushSubscription = Iceshrimp.WebPush.PushSubscription;

View file

@ -22,13 +22,13 @@ public class QueueService(
) : BackgroundService ) : BackgroundService
{ {
private readonly List<IPostgresJobQueue> _queues = []; private readonly List<IPostgresJobQueue> _queues = [];
public readonly BackgroundTaskQueue BackgroundTaskQueue = new(queueConcurrency.Value.BackgroundTask);
public IEnumerable<string> QueueNames => _queues.Select(p => p.Name); public readonly DeliverQueue DeliverQueue = new(queueConcurrency.Value.Deliver);
public readonly InboxQueue InboxQueue = new(queueConcurrency.Value.Inbox); public readonly InboxQueue InboxQueue = new(queueConcurrency.Value.Inbox);
public readonly DeliverQueue DeliverQueue = new(queueConcurrency.Value.Deliver);
public readonly PreDeliverQueue PreDeliverQueue = new(queueConcurrency.Value.PreDeliver); public readonly PreDeliverQueue PreDeliverQueue = new(queueConcurrency.Value.PreDeliver);
public readonly BackgroundTaskQueue BackgroundTaskQueue = new(queueConcurrency.Value.BackgroundTask);
public IEnumerable<string> QueueNames => _queues.Select(p => p.Name);
private static async Task<NpgsqlConnection> GetNpgsqlConnection(IServiceScope scope) private static async Task<NpgsqlConnection> GetNpgsqlConnection(IServiceScope scope)
{ {
@ -263,13 +263,13 @@ public interface IPostgresJobQueue
{ {
public string Name { get; } public string Name { get; }
public TimeSpan Timeout { get; }
public Task ExecuteAsync(IServiceScopeFactory scopeFactory, CancellationToken token, CancellationToken queueToken); public Task ExecuteAsync(IServiceScopeFactory scopeFactory, CancellationToken token, CancellationToken queueToken);
public Task RecoverOrPrepareForExitAsync(); public Task RecoverOrPrepareForExitAsync();
public void RaiseJobQueuedEvent(); public void RaiseJobQueuedEvent();
public void RaiseJobDelayedEvent(); public void RaiseJobDelayedEvent();
public TimeSpan Timeout { get; }
} }
public class PostgresJobQueue<T>( public class PostgresJobQueue<T>(
@ -279,14 +279,13 @@ public class PostgresJobQueue<T>(
TimeSpan timeout TimeSpan timeout
) : IPostgresJobQueue where T : class ) : IPostgresJobQueue where T : class
{ {
public string Name => name;
public TimeSpan Timeout => timeout;
private readonly AsyncAutoResetEvent _delayedChannel = new(); private readonly AsyncAutoResetEvent _delayedChannel = new();
private readonly AsyncAutoResetEvent _queuedChannel = new(); private readonly AsyncAutoResetEvent _queuedChannel = new();
private IServiceScopeFactory _scopeFactory = null!;
private ILogger<QueueService> _logger = null!; private ILogger<QueueService> _logger = null!;
private IServiceScopeFactory _scopeFactory = null!;
private string? _workerId; private string? _workerId;
public string Name => name;
public TimeSpan Timeout => timeout;
public void RaiseJobQueuedEvent() => QueuedChannelEvent?.Invoke(null, EventArgs.Empty); public void RaiseJobQueuedEvent() => QueuedChannelEvent?.Invoke(null, EventArgs.Empty);
public void RaiseJobDelayedEvent() => DelayedChannelEvent?.Invoke(null, EventArgs.Empty); public void RaiseJobDelayedEvent() => DelayedChannelEvent?.Invoke(null, EventArgs.Empty);

View file

@ -12,20 +12,17 @@ namespace Iceshrimp.Backend.Core.Services;
public sealed class StreamingService public sealed class StreamingService
{ {
private readonly ConcurrentDictionary<string, StreamingConnectionAggregate> _connections = [];
private readonly IHubContext<StreamingHub, IStreamingHubClient> _hub;
private readonly EventService _eventSvc;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<StreamingService> _logger;
private static readonly AsyncKeyedLocker<string> Locker = new(o => private static readonly AsyncKeyedLocker<string> Locker = new(o =>
{ {
o.PoolSize = 100; o.PoolSize = 100;
o.PoolInitialFill = 5; o.PoolInitialFill = 5;
}); });
public event EventHandler<(Note note, Func<Task<NoteResponse>> rendered)>? NotePublished; private readonly ConcurrentDictionary<string, StreamingConnectionAggregate> _connections = [];
public event EventHandler<(Note note, Func<Task<NoteResponse>> rendered)>? NoteUpdated; private readonly EventService _eventSvc;
private readonly IHubContext<StreamingHub, IStreamingHubClient> _hub;
private readonly ILogger<StreamingService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public StreamingService( public StreamingService(
IHubContext<StreamingHub, IStreamingHubClient> hub, IHubContext<StreamingHub, IStreamingHubClient> hub,
@ -43,6 +40,9 @@ public sealed class StreamingService
eventSvc.NoteUpdated += OnNoteUpdated; eventSvc.NoteUpdated += OnNoteUpdated;
} }
public event EventHandler<(Note note, Func<Task<NoteResponse>> rendered)>? NotePublished;
public event EventHandler<(Note note, Func<Task<NoteResponse>> rendered)>? NoteUpdated;
public void Connect(string userId, User user, string connectionId) public void Connect(string userId, User user, string connectionId)
{ {
_connections _connections

View file

@ -5,8 +5,8 @@ using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing; using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
using static Iceshrimp.Parsing.MfmNodeTypes;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Services;

View file

@ -166,7 +166,7 @@ public class UserService(
//TODO: FollowersCount //TODO: FollowersCount
//TODO: FollowingCount //TODO: FollowingCount
Emojis = emoji.Select(p => p.Id).ToList(), Emojis = emoji.Select(p => p.Id).ToList(),
Tags = tags, Tags = tags
}; };
var profile = new UserProfile var profile = new UserProfile

View file

@ -1,5 +1,6 @@
using Iceshrimp.Shared.HubSchemas; using Iceshrimp.Shared.HubSchemas;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace Iceshrimp.Backend.Hubs; namespace Iceshrimp.Backend.Hubs;
public class ExampleHub : Hub<IExampleHubClient>, IExampleHubServer public class ExampleHub : Hub<IExampleHubClient>, IExampleHubServer

View file

@ -18,114 +18,49 @@ namespace Iceshrimp.Backend.Hubs.Helpers;
[MustDisposeResource] [MustDisposeResource]
public sealed class StreamingConnectionAggregate : IDisposable public sealed class StreamingConnectionAggregate : IDisposable
{ {
private readonly User _user; private readonly WriteLockingList<string> _blockedBy = [];
private readonly string _userId; private readonly WriteLockingList<string> _blocking = [];
private readonly WriteLockingList<string> _connectionIds = []; private readonly WriteLockingList<string> _connectionIds = [];
private readonly IHubContext<StreamingHub, IStreamingHubClient> _hub;
private readonly EventService _eventService; private readonly EventService _eventService;
private readonly WriteLockingList<string> _following = [];
private readonly IHubContext<StreamingHub, IStreamingHubClient> _hub;
private readonly ILogger _logger;
private readonly WriteLockingList<string> _muting = [];
private readonly IServiceScope _scope; private readonly IServiceScope _scope;
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory;
private readonly StreamingService _streamingService; private readonly StreamingService _streamingService;
private readonly ILogger _logger;
private readonly WriteLockingList<string> _following = [];
private readonly WriteLockingList<string> _muting = [];
private readonly WriteLockingList<string> _blocking = [];
private readonly WriteLockingList<string> _blockedBy = [];
private List<string> _hiddenFromHome = [];
private readonly ConcurrentDictionary<string, WriteLockingList<StreamingTimeline>> _subscriptions = []; private readonly ConcurrentDictionary<string, WriteLockingList<StreamingTimeline>> _subscriptions = [];
private readonly User _user;
private readonly string _userId;
private List<string> _hiddenFromHome = [];
public bool HasSubscribers => _connectionIds.Count != 0; public bool HasSubscribers => _connectionIds.Count != 0;
#region Destruction
public void Dispose()
{
DisconnectAll();
_streamingService.NotePublished -= OnNotePublished;
_streamingService.NoteUpdated -= OnNoteUpdated;
_eventService.Notification -= OnNotification;
_eventService.UserBlocked -= OnUserBlock;
_eventService.UserUnblocked -= OnUserUnblock;
_eventService.UserMuted -= OnUserMute;
_eventService.UserUnmuted -= OnUserUnmute;
_eventService.UserFollowed -= OnUserFollow;
_eventService.UserUnfollowed -= OnUserUnfollow;
_scope.Dispose();
}
#endregion
private AsyncServiceScope GetTempScope() => _scopeFactory.CreateAsyncScope(); private AsyncServiceScope GetTempScope() => _scopeFactory.CreateAsyncScope();
#region Initialization
public StreamingConnectionAggregate(
string userId,
User user,
IHubContext<StreamingHub, IStreamingHubClient> hub,
EventService eventSvc,
IServiceScopeFactory scopeFactory, StreamingService streamingService
)
{
if (userId != user.Id)
throw new Exception("userId doesn't match user.Id");
_userId = userId;
_user = user;
_hub = hub;
_eventService = eventSvc;
_scope = scopeFactory.CreateScope();
_scopeFactory = scopeFactory;
_streamingService = streamingService;
_logger = _scope.ServiceProvider.GetRequiredService<ILogger<StreamingConnectionAggregate>>();
_ = InitializeAsync();
}
private async Task InitializeAsync()
{
_eventService.UserBlocked += OnUserBlock;
_eventService.UserUnblocked += OnUserUnblock;
_eventService.UserMuted += OnUserMute;
_eventService.UserUnmuted += OnUserUnmute;
_eventService.UserFollowed += OnUserFollow;
_eventService.UserUnfollowed += OnUserUnfollow;
await InitializeRelationships();
_eventService.Notification += OnNotification;
_streamingService.NotePublished += OnNotePublished;
_streamingService.NoteUpdated += OnNoteUpdated;
}
private async Task InitializeRelationships()
{
await using var scope = GetTempScope();
await using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
_following.AddRange(await db.Followings.Where(p => p.Follower == _user)
.Select(p => p.FolloweeId)
.ToListAsync());
_blocking.AddRange(await db.Blockings.Where(p => p.Blocker == _user)
.Select(p => p.BlockeeId)
.ToListAsync());
_blockedBy.AddRange(await db.Blockings.Where(p => p.Blockee == _user)
.Select(p => p.BlockerId)
.ToListAsync());
_muting.AddRange(await db.Mutings.Where(p => p.Muter == _user)
.Select(p => p.MuteeId)
.ToListAsync());
_hiddenFromHome = await db.UserListMembers
.Where(p => p.UserList.User == _user && p.UserList.HideFromHomeTl)
.Select(p => p.UserId)
.Distinct()
.ToListAsync();
}
#endregion
#region Channel subscription handlers
public void Subscribe(string connectionId, StreamingTimeline timeline)
{
if (!_connectionIds.Contains(connectionId)) return;
_subscriptions.GetOrAdd(connectionId, []).Add(timeline);
}
public void Unsubscribe(string connectionId, StreamingTimeline timeline)
{
if (!_connectionIds.Contains(connectionId)) return;
_subscriptions.TryGetValue(connectionId, out var collection);
collection?.Remove(timeline);
}
#endregion
private async void OnNotification(object? _, Notification notification) private async void OnNotification(object? _, Notification notification)
{ {
try try
@ -228,13 +163,6 @@ public sealed class StreamingConnectionAggregate : IDisposable
return wrapped; return wrapped;
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Reply = note.Reply;
public Note? Renote = note.Renote;
}
private (List<string> connectionIds, List<StreamingTimeline> timelines) FindRecipients(Note note) private (List<string> connectionIds, List<StreamingTimeline> timelines) FindRecipients(Note note)
{ {
List<StreamingTimeline> timelines = []; List<StreamingTimeline> timelines = [];
@ -260,6 +188,97 @@ public sealed class StreamingConnectionAggregate : IDisposable
return (connectionIds, timelines); return (connectionIds, timelines);
} }
private class NoteWithVisibilities(Note note)
{
public readonly Note Note = note;
public Note? Renote = note.Renote;
public Note? Reply = note.Reply;
}
#region Initialization
public StreamingConnectionAggregate(
string userId,
User user,
IHubContext<StreamingHub, IStreamingHubClient> hub,
EventService eventSvc,
IServiceScopeFactory scopeFactory, StreamingService streamingService
)
{
if (userId != user.Id)
throw new Exception("userId doesn't match user.Id");
_userId = userId;
_user = user;
_hub = hub;
_eventService = eventSvc;
_scope = scopeFactory.CreateScope();
_scopeFactory = scopeFactory;
_streamingService = streamingService;
_logger = _scope.ServiceProvider.GetRequiredService<ILogger<StreamingConnectionAggregate>>();
_ = InitializeAsync();
}
private async Task InitializeAsync()
{
_eventService.UserBlocked += OnUserBlock;
_eventService.UserUnblocked += OnUserUnblock;
_eventService.UserMuted += OnUserMute;
_eventService.UserUnmuted += OnUserUnmute;
_eventService.UserFollowed += OnUserFollow;
_eventService.UserUnfollowed += OnUserUnfollow;
await InitializeRelationships();
_eventService.Notification += OnNotification;
_streamingService.NotePublished += OnNotePublished;
_streamingService.NoteUpdated += OnNoteUpdated;
}
private async Task InitializeRelationships()
{
await using var scope = GetTempScope();
await using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
_following.AddRange(await db.Followings.Where(p => p.Follower == _user)
.Select(p => p.FolloweeId)
.ToListAsync());
_blocking.AddRange(await db.Blockings.Where(p => p.Blocker == _user)
.Select(p => p.BlockeeId)
.ToListAsync());
_blockedBy.AddRange(await db.Blockings.Where(p => p.Blockee == _user)
.Select(p => p.BlockerId)
.ToListAsync());
_muting.AddRange(await db.Mutings.Where(p => p.Muter == _user)
.Select(p => p.MuteeId)
.ToListAsync());
_hiddenFromHome = await db.UserListMembers
.Where(p => p.UserList.User == _user && p.UserList.HideFromHomeTl)
.Select(p => p.UserId)
.Distinct()
.ToListAsync();
}
#endregion
#region Channel subscription handlers
public void Subscribe(string connectionId, StreamingTimeline timeline)
{
if (!_connectionIds.Contains(connectionId)) return;
_subscriptions.GetOrAdd(connectionId, []).Add(timeline);
}
public void Unsubscribe(string connectionId, StreamingTimeline timeline)
{
if (!_connectionIds.Contains(connectionId)) return;
_subscriptions.TryGetValue(connectionId, out var collection);
collection?.Remove(timeline);
}
#endregion
#region Relationship change event handlers #region Relationship change event handlers
private void OnUserBlock(object? _, UserInteraction interaction) private void OnUserBlock(object? _, UserInteraction interaction)
@ -373,23 +392,4 @@ public sealed class StreamingConnectionAggregate : IDisposable
} }
#endregion #endregion
#region Destruction
public void Dispose()
{
DisconnectAll();
_streamingService.NotePublished -= OnNotePublished;
_streamingService.NoteUpdated -= OnNoteUpdated;
_eventService.Notification -= OnNotification;
_eventService.UserBlocked -= OnUserBlock;
_eventService.UserUnblocked -= OnUserUnblock;
_eventService.UserMuted -= OnUserMute;
_eventService.UserUnmuted -= OnUserUnmute;
_eventService.UserFollowed -= OnUserFollow;
_eventService.UserUnfollowed -= OnUserUnfollow;
_scope.Dispose();
}
#endregion
} }

View file

@ -1,5 +1,5 @@
@page "/" @page "/"
@model Iceshrimp.Backend.Pages.IndexModel @model IndexModel
@{ @{
ViewData["title"] = $"Home - {Model.InstanceName}"; ViewData["title"] = $"Home - {Model.InstanceName}";

View file

@ -7,9 +7,9 @@ namespace Iceshrimp.Backend.Pages;
public class IndexModel(MetaService meta) : PageModel public class IndexModel(MetaService meta) : PageModel
{ {
public string InstanceName = null!;
public string InstanceDescription = null!;
public string? ContactEmail; public string? ContactEmail;
public string InstanceDescription = null!;
public string InstanceName = null!;
public async Task<IActionResult> OnGet() public async Task<IActionResult> OnGet()
{ {

View file

@ -1,7 +1,7 @@
@page "/notes/{id}" @page "/notes/{id}"
@using Iceshrimp.Backend.Core.Database.Tables @using Iceshrimp.Backend.Core.Database.Tables
@using Iceshrimp.Backend.Core.Extensions @using Iceshrimp.Backend.Core.Extensions
@model Iceshrimp.Backend.Pages.NoteModel @model NoteModel
@if (Model.Note == null) @if (Model.Note == null)
{ {

View file

@ -19,13 +19,13 @@ public class NoteModel(
MfmConverter mfmConverter MfmConverter mfmConverter
) : PageModel ) : PageModel
{ {
public Dictionary<string, List<DriveFile>> MediaAttachments = new();
public Note? Note; public Note? Note;
public string? QuoteUrl; public string? QuoteUrl;
public Dictionary<string, string> TextContent = new();
public Dictionary<string, List<DriveFile>> MediaAttachments = new();
public bool ShowMedia = security.Value.PublicPreview > Enums.PublicPreview.RestrictedNoMedia; public bool ShowMedia = security.Value.PublicPreview > Enums.PublicPreview.RestrictedNoMedia;
public bool ShowRemoteReplies = security.Value.PublicPreview > Enums.PublicPreview.Restricted; public bool ShowRemoteReplies = security.Value.PublicPreview > Enums.PublicPreview.Restricted;
public Dictionary<string, string> TextContent = new();
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", [SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery",
Justification = "IncludeCommonProperties")] Justification = "IncludeCommonProperties")]

View file

@ -24,6 +24,7 @@
width: 3em; width: 3em;
max-height: 3em; max-height: 3em;
} }
.title { .title {
grid-column: 2/-1; grid-column: 2/-1;
grid-row: 1; grid-row: 1;
@ -31,6 +32,7 @@
color: var(--text-bright); color: var(--text-bright);
font-weight: 600; font-weight: 600;
} }
.acct { .acct {
grid-column: 2/-1; grid-column: 2/-1;
grid-row: 2; grid-row: 2;

View file

@ -1,8 +1,8 @@
@page "/queue/{queue?}/{pagination:int?}/{status?}" @page "/queue/{queue?}/{pagination:int?}/{status?}"
@inject QueueService QueueSvc
@using Iceshrimp.Backend.Core.Database.Tables @using Iceshrimp.Backend.Core.Database.Tables
@using Iceshrimp.Backend.Core.Extensions @using Iceshrimp.Backend.Core.Extensions
@using Iceshrimp.Backend.Core.Services @using Iceshrimp.Backend.Core.Services
@inject QueueService QueueSvc
@model QueueModel @model QueueModel
@{ @{

View file

@ -10,15 +10,15 @@ namespace Iceshrimp.Backend.Pages;
public class QueueModel(DatabaseContext db, QueueService queueSvc) : PageModel public class QueueModel(DatabaseContext db, QueueService queueSvc) : PageModel
{ {
public List<Job> Jobs = []; public int? DelayedCount;
public string? Queue;
public Job.JobStatus? Filter; public Job.JobStatus? Filter;
public int? TotalCount; public List<Job> Jobs = [];
public int? NextPage;
public int? PrevPage;
public string? Queue;
public int? QueuedCount; public int? QueuedCount;
public int? RunningCount; public int? RunningCount;
public int? DelayedCount; public int? TotalCount;
public int? PrevPage;
public int? NextPage;
public async Task<IActionResult> OnGet( public async Task<IActionResult> OnGet(
[FromRoute] string? queue, [FromRoute(Name = "pagination")] int? page, [FromRoute] string? status [FromRoute] string? queue, [FromRoute(Name = "pagination")] int? page, [FromRoute] string? status

View file

@ -42,7 +42,9 @@
{ {
<tr> <tr>
<td>Actions</td> <td>Actions</td>
<td><a class="fake-link" onclick="retry('@Model.Job.Id.ToStringLower()')">Retry</a></td> <td>
<a class="fake-link" onclick="retry('@Model.Job.Id.ToStringLower()')">Retry</a>
</td>
</tr> </tr>
} }
<tr> <tr>

View file

@ -9,8 +9,17 @@ namespace Iceshrimp.Backend.Pages;
public class QueueJobModel(DatabaseContext db) : PageModel public class QueueJobModel(DatabaseContext db) : PageModel
{ {
private static Dictionary<string, string> _lookup = new()
{
{ "inbox", "body" },
{ "deliver", "payload" },
{ "pre-deliver", "serializedActivity" }
};
public Job Job = null!; public Job Job = null!;
public Dictionary<string, string> Lookup => _lookup;
public async Task<IActionResult> OnGet([FromRoute] Guid id) public async Task<IActionResult> OnGet([FromRoute] Guid id)
{ {
if (!Request.Cookies.TryGetValue("admin_session", out var cookie)) if (!Request.Cookies.TryGetValue("admin_session", out var cookie))
@ -22,13 +31,4 @@ public class QueueJobModel(DatabaseContext db) : PageModel
throw GracefulException.NotFound($"Job {id} not found"); throw GracefulException.NotFound($"Job {id} not found");
return Page(); return Page();
} }
private static Dictionary<string, string> _lookup = new()
{
{ "inbox", "body" },
{ "deliver", "payload" },
{ "pre-deliver", "serializedActivity" }
};
public Dictionary<string, string> Lookup => _lookup;
} }