From bf3e72da9b88226a510eed92000657f27f96d689 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 25 Jan 2024 03:16:29 +0100 Subject: [PATCH] Basic note create --- .../Core/Database/DatabaseContext.cs | 20 +++++++------ .../Core/Database/Tables/Note.cs | 30 +++++++++---------- .../Core/Extensions/ServiceExtensions.cs | 7 +++++ .../ActivityStreams/Types/ASNote.cs | 4 +++ .../ActivityStreams/Types/LDIdObject.cs | 2 +- .../Federation/Cryptography/HttpSignature.cs | 14 +++++---- .../Middleware/AuthorizedFetchMiddleware.cs | 2 +- Iceshrimp.Backend/Core/Queues/InboxQueue.cs | 4 ++- .../Core/Services/NoteService.cs | 30 +++++++++++++++++-- .../Core/Services/QueueService.cs | 11 +++++-- Iceshrimp.Backend/Startup.cs | 3 +- 11 files changed, 87 insertions(+), 40 deletions(-) diff --git a/Iceshrimp.Backend/Core/Database/DatabaseContext.cs b/Iceshrimp.Backend/Core/Database/DatabaseContext.cs index 2e2c9dcb..090f450e 100644 --- a/Iceshrimp.Backend/Core/Database/DatabaseContext.cs +++ b/Iceshrimp.Backend/Core/Database/DatabaseContext.cs @@ -2,15 +2,13 @@ using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database.Tables; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; using Npgsql; namespace Iceshrimp.Backend.Core.Database; [SuppressMessage("ReSharper", "StringLiteralTypo")] [SuppressMessage("ReSharper", "IdentifierTypo")] -public class DatabaseContext(DbContextOptions options, IOptions config) - : DbContext(options) { +public class DatabaseContext(DbContextOptions options) : DbContext(options) { public virtual DbSet AbuseUserReports { get; init; } = null!; public virtual DbSet AccessTokens { get; init; } = null!; public virtual DbSet Announcements { get; init; } = null!; @@ -79,16 +77,16 @@ public class DatabaseContext(DbContextOptions options, IOptions public virtual DbSet UserSecurityKeys { get; init; } = null!; public virtual DbSet Webhooks { get; init; } = null!; - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + public static NpgsqlDataSource GetDataSource(Config.DatabaseSection? config) { var dataSourceBuilder = new NpgsqlDataSourceBuilder(); if (config == null) throw new Exception("Failed to initialize database: Failed to load configuration"); - dataSourceBuilder.ConnectionStringBuilder.Host = config.Value.Host; - dataSourceBuilder.ConnectionStringBuilder.Username = config.Value.Username; - dataSourceBuilder.ConnectionStringBuilder.Password = config.Value.Password; - dataSourceBuilder.ConnectionStringBuilder.Database = config.Value.Database; + dataSourceBuilder.ConnectionStringBuilder.Host = config.Host; + dataSourceBuilder.ConnectionStringBuilder.Username = config.Username; + dataSourceBuilder.ConnectionStringBuilder.Password = config.Password; + dataSourceBuilder.ConnectionStringBuilder.Database = config.Database; dataSourceBuilder.MapEnum(); dataSourceBuilder.MapEnum(); @@ -99,7 +97,11 @@ public class DatabaseContext(DbContextOptions options, IOptions dataSourceBuilder.MapEnum(); dataSourceBuilder.MapEnum(); // FIXME: WHY IS THIS ITS OWN ENUM - optionsBuilder.UseNpgsql(dataSourceBuilder.Build()); + return dataSourceBuilder.Build(); + } + + public static void Configure(DbContextOptionsBuilder optionsBuilder, NpgsqlDataSource dataSource) { + optionsBuilder.UseNpgsql(dataSource); } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Iceshrimp.Backend/Core/Database/Tables/Note.cs b/Iceshrimp.Backend/Core/Database/Tables/Note.cs index 53b4fe9b..c0e2b264 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/Note.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/Note.cs @@ -24,6 +24,15 @@ namespace Iceshrimp.Backend.Core.Database.Tables; [Index("Url")] [Index("UserId", "Id")] public class Note { + [PgName("note_visibility_enum")] + public enum NoteVisibility { + [PgName("public")] Public, + [PgName("home")] Home, + [PgName("followers")] Followers, + [PgName("specified")] Specified, + [PgName("hidden")] Hidden + } + [Key] [Column("id")] [StringLength(32)] @@ -83,24 +92,24 @@ public class Note { [Column("score")] public int Score { get; set; } [Column("fileIds", TypeName = "character varying(32)[]")] - public List FileIds { get; set; } = null!; + public List FileIds { get; set; } = []; [Column("attachedFileTypes", TypeName = "character varying(256)[]")] - public List AttachedFileTypes { get; set; } = null!; + public List AttachedFileTypes { get; set; } = []; [Column("visibleUserIds", TypeName = "character varying(32)[]")] - public List VisibleUserIds { get; set; } = null!; + public List VisibleUserIds { get; set; } = []; [Column("mentions", TypeName = "character varying(32)[]")] - public List Mentions { get; set; } = null!; + public List Mentions { get; set; } = []; [Column("mentionedRemoteUsers")] public string MentionedRemoteUsers { get; set; } = null!; [Column("emojis", TypeName = "character varying(128)[]")] - public List Emojis { get; set; } = null!; + public List Emojis { get; set; } = []; [Column("tags", TypeName = "character varying(128)[]")] - public List Tags { get; set; } = null!; + public List Tags { get; set; } = []; [Column("hasPoll")] public bool HasPoll { get; set; } @@ -216,13 +225,4 @@ public class Note { [InverseProperty("Note")] public virtual ICollection UserNotePins { get; set; } = new List(); - - [PgName("note_visibility_enum")] - public enum NoteVisibility { - [PgName("public")] Public, - [PgName("home")] Home, - [PgName("followers")] Followers, - [PgName("specified")] Specified, - [PgName("hidden")] Hidden - } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index 7dc973a0..24dd2e3e 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -1,5 +1,6 @@ using Iceshrimp.Backend.Controllers.Renderers.ActivityPub; using Iceshrimp.Backend.Core.Configuration; +using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Federation.ActivityPub; using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Middleware; @@ -42,4 +43,10 @@ public static class ServiceExtensions { services.Configure(configuration.GetSection("Security")); services.Configure(configuration.GetSection("Database")); } + + public static void AddDatabaseContext(this IServiceCollection services, IConfiguration configuration) { + var config = configuration.GetSection("Database").Get(); + var dataSource = DatabaseContext.GetDataSource(config); + services.AddDbContext(options => { DatabaseContext.Configure(options, dataSource); }); + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs index 331151d5..6c3c7ba5 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs @@ -13,6 +13,10 @@ public class ASNote : ASObject { [JC(typeof(VC))] public string? Content { get; set; } + [J("https://www.w3.org/ns/activitystreams#url")] + [JC(typeof(LDIdObjectConverter))] + public LDIdObject? Url { get; set; } + [J("https://www.w3.org/ns/activitystreams#sensitive")] [JC(typeof(VC))] public bool? Sensitive { get; set; } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDIdObject.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDIdObject.cs index 8d46167b..a566b80b 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDIdObject.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDIdObject.cs @@ -14,4 +14,4 @@ public class LDIdObject() { } } -public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter; \ No newline at end of file +public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter; \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs index bcc77468..f6683311 100644 --- a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs @@ -17,8 +17,9 @@ public static class HttpSignature { request.Path, request.Headers); - request.Body.Position = 0; - return await VerifySignature(key, signingString, signature, request.Headers, request.Body); + if (request.Body.CanSeek) request.Body.Position = 0; + return await VerifySignature(key, signingString, signature, request.Headers, + request.ContentLength > 0 ? request.Body : null); } public static async Task Verify(this HttpRequestMessage request, string key) { @@ -114,18 +115,19 @@ public static class HttpSignature { //TODO: these fail if the dictionary doesn't contain the key, use TryGetValue instead var signatureBase64 = sig["signature"] ?? throw new GracefulException(HttpStatusCode.Forbidden, - "Signature string is missing the signature field"); + "Signature string is missing the signature field"); var headers = sig["headers"].Split(" ") ?? throw new GracefulException(HttpStatusCode.Forbidden, - "Signature data is missing the headers field"); + "Signature data is missing the headers field"); var keyId = sig["keyId"] ?? - throw new GracefulException(HttpStatusCode.Forbidden, "Signature string is missing the keyId field"); + throw new GracefulException(HttpStatusCode.Forbidden, + "Signature string is missing the keyId field"); //TODO: this should fallback to sha256 var algo = sig["algorithm"] ?? throw new GracefulException(HttpStatusCode.Forbidden, - "Signature string is missing the algorithm field"); + "Signature string is missing the algorithm field"); var signature = Convert.FromBase64String(signatureBase64); diff --git a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs index 6051a50d..804e5230 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs @@ -37,7 +37,7 @@ public class AuthorizedFetchMiddleware( // If we still don't have the key, something went wrong and we need to throw an exception if (key == null) throw new GracefulException("Failed to fetch key of signature user"); - List headers = request.Body.Length > 0 || attribute.ForceBody + List headers = request.ContentLength > 0 || attribute.ForceBody ? ["(request-target)", "digest", "host", "date"] : ["(request-target)", "host", "date"]; diff --git a/Iceshrimp.Backend/Core/Queues/InboxQueue.cs b/Iceshrimp.Backend/Core/Queues/InboxQueue.cs index 24d9938d..f84adb24 100644 --- a/Iceshrimp.Backend/Core/Queues/InboxQueue.cs +++ b/Iceshrimp.Backend/Core/Queues/InboxQueue.cs @@ -18,7 +18,9 @@ public class InboxQueue { if (obj == null) throw new Exception("Failed to deserialize ASObject"); if (obj is not ASActivity activity) throw new NotImplementedException("Job data is not an ASActivity"); - var apSvc = scope.GetRequiredService(); + var apSvc = scope.GetRequiredService(); + var logger = scope.GetRequiredService>(); + logger.LogTrace("Preparation took {ms} ms", job.Duration); await apSvc.PerformActivity(activity, job.InboxUserId); } } diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 8204bc45..1bb071a3 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -1,14 +1,40 @@ using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Federation.ActivityPub; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; +using Iceshrimp.Backend.Core.Helpers; +using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Services; public class NoteService(ILogger logger, DatabaseContext db, UserResolver userResolver) { public async Task CreateNote(ASNote note, ASActor actor) { + if (await db.Notes.AnyAsync(p => p.Uri == note.Id)) { + logger.LogDebug("Note '{id}' already exists, skipping", note.Id); + return; + } + + logger.LogDebug("Creating note: {id}", note.Id); + var user = await userResolver.Resolve(actor.Id); - logger.LogDebug("Resolved user to {user}", user.Id); - //TODO: insert into database + logger.LogDebug("Resolved user to {userId}", user.Id); + //TODO: resolve anything related to the note as well (reply thread, attachments, emoji, etc) + + var dbNote = new Note { + Id = IdHelpers.GenerateSlowflakeId(), + Uri = note.Id, + Url = note.Url?.Id, //FIXME: this doesn't seem to work yet + Text = note.MkContent ?? note.Content, //TODO: html-to-mfm + UserId = user.Id, + CreatedAt = note.PublishedAt?.ToUniversalTime() ?? + throw new Exception("Missing or invalid PublishedAt field"), + UserHost = user.Host, + Visibility = Note.NoteVisibility.Public //TODO: parse to & cc fields + }; + + await db.Notes.AddAsync(dbNote); + await db.SaveChangesAsync(); + logger.LogDebug("Note {id} created successfully", dbNote.Id); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/QueueService.cs b/Iceshrimp.Backend/Core/Services/QueueService.cs index 508396ec..c561d3eb 100644 --- a/Iceshrimp.Backend/Core/Services/QueueService.cs +++ b/Iceshrimp.Backend/Core/Services/QueueService.cs @@ -50,10 +50,10 @@ public class JobQueue(string name, Func>(); - logger.LogError("Failed to process job in {queue} queue: {error}", name, _queue); + logger.LogError("Failed to process job in {queue} queue: {error}", name, e.Message); } - if (job.Status is Job.JobStatus.Completed or Job.JobStatus.Failed) { + if (job.Status is Job.JobStatus.Failed) { job.FinishedAt = DateTime.Now; } else if (job.Status is Job.JobStatus.Delayed && job.DelayedUntil == null) { @@ -62,7 +62,11 @@ public class JobQueue(string name, Func>(); + logger.LogTrace("Job in queue {queue} completed after {ms} ms", name, job.Duration); } scope.Dispose(); @@ -99,6 +103,7 @@ public abstract class Job { public DateTime QueuedAt = DateTime.Now; public JobStatus Status = JobStatus.Queued; + public long Duration => (long)((FinishedAt ?? DateTime.Now) - QueuedAt).TotalMilliseconds; } //TODO: handle delayed jobs diff --git a/Iceshrimp.Backend/Startup.cs b/Iceshrimp.Backend/Startup.cs index 910fe956..92328d2d 100644 --- a/Iceshrimp.Backend/Startup.cs +++ b/Iceshrimp.Backend/Startup.cs @@ -1,5 +1,4 @@ using Asp.Versioning; -using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Extensions; using Vite.AspNetCore.Extensions; @@ -32,7 +31,7 @@ builder.Services.AddViteServices(options => { }); //TODO: single line only if there's no \n in the log msg (otherwise stacktraces don't work) builder.Services.AddLogging(logging => logging.AddSimpleConsole(options => { options.SingleLine = true; })); -builder.Services.AddDbContext(); +builder.Services.AddDatabaseContext(builder.Configuration); //TODO: maybe use a dbcontext factory? builder.Services.AddServices(); builder.Services.ConfigureServices(builder.Configuration);