Basic note create

This commit is contained in:
Laura Hausmann 2024-01-25 03:16:29 +01:00
parent 8d4a20896c
commit bf3e72da9b
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
11 changed files with 87 additions and 40 deletions

View file

@ -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<DatabaseContext> options, IOptions<Config.DatabaseSection> config)
: DbContext(options) {
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options) {
public virtual DbSet<AbuseUserReport> AbuseUserReports { get; init; } = null!;
public virtual DbSet<AccessToken> AccessTokens { get; init; } = null!;
public virtual DbSet<Announcement> Announcements { get; init; } = null!;
@ -79,16 +77,16 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options, IOptions
public virtual DbSet<UserSecurityKey> UserSecurityKeys { get; init; } = null!;
public virtual DbSet<Webhook> 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<Antenna.AntennaSource>();
dataSourceBuilder.MapEnum<Note.NoteVisibility>();
@ -99,7 +97,11 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options, IOptions
dataSourceBuilder.MapEnum<UserProfile.UserProfileFFVisibility>();
dataSourceBuilder.MapEnum<UserProfile.MutingNotificationType>(); // 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) {

View file

@ -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<string> FileIds { get; set; } = null!;
public List<string> FileIds { get; set; } = [];
[Column("attachedFileTypes", TypeName = "character varying(256)[]")]
public List<string> AttachedFileTypes { get; set; } = null!;
public List<string> AttachedFileTypes { get; set; } = [];
[Column("visibleUserIds", TypeName = "character varying(32)[]")]
public List<string> VisibleUserIds { get; set; } = null!;
public List<string> VisibleUserIds { get; set; } = [];
[Column("mentions", TypeName = "character varying(32)[]")]
public List<string> Mentions { get; set; } = null!;
public List<string> Mentions { get; set; } = [];
[Column("mentionedRemoteUsers")] public string MentionedRemoteUsers { get; set; } = null!;
[Column("emojis", TypeName = "character varying(128)[]")]
public List<string> Emojis { get; set; } = null!;
public List<string> Emojis { get; set; } = [];
[Column("tags", TypeName = "character varying(128)[]")]
public List<string> Tags { get; set; } = null!;
public List<string> Tags { get; set; } = [];
[Column("hasPoll")] public bool HasPoll { get; set; }
@ -216,13 +225,4 @@ public class Note {
[InverseProperty("Note")]
public virtual ICollection<UserNotePin> UserNotePins { get; set; } = new List<UserNotePin>();
[PgName("note_visibility_enum")]
public enum NoteVisibility {
[PgName("public")] Public,
[PgName("home")] Home,
[PgName("followers")] Followers,
[PgName("specified")] Specified,
[PgName("hidden")] Hidden
}
}

View file

@ -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<Config.SecuritySection>(configuration.GetSection("Security"));
services.Configure<Config.DatabaseSection>(configuration.GetSection("Database"));
}
public static void AddDatabaseContext(this IServiceCollection services, IConfiguration configuration) {
var config = configuration.GetSection("Database").Get<Config.DatabaseSection>();
var dataSource = DatabaseContext.GetDataSource(config);
services.AddDbContext<DatabaseContext>(options => { DatabaseContext.Configure(options, dataSource); });
}
}

View file

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

View file

@ -14,4 +14,4 @@ public class LDIdObject() {
}
}
public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter<ASLink>;
public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter<LDIdObject>;

View file

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

View file

@ -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<string> headers = request.Body.Length > 0 || attribute.ForceBody
List<string> headers = request.ContentLength > 0 || attribute.ForceBody
? ["(request-target)", "digest", "host", "date"]
: ["(request-target)", "host", "date"];

View file

@ -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<APService>();
var apSvc = scope.GetRequiredService<APService>();
var logger = scope.GetRequiredService<ILogger<InboxQueue>>();
logger.LogTrace("Preparation took {ms} ms", job.Duration);
await apSvc.PerformActivity(activity, job.InboxUserId);
}
}

View file

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

View file

@ -50,10 +50,10 @@ public class JobQueue<T>(string name, Func<T, IServiceProvider, CancellationToke
job.Exception = e;
var logger = scope.ServiceProvider.GetRequiredService<ILogger<QueueService>>();
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<T>(string name, Func<T, IServiceProvider, CancellationToke
job.FinishedAt = DateTime.Now;
}
else {
job.Status = Job.JobStatus.Completed;
job.Status = Job.JobStatus.Completed;
job.FinishedAt = DateTime.Now;
var logger = scope.ServiceProvider.GetRequiredService<ILogger<QueueService>>();
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

View file

@ -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<DatabaseContext>();
builder.Services.AddDatabaseContext(builder.Configuration); //TODO: maybe use a dbcontext factory?
builder.Services.AddServices();
builder.Services.ConfigureServices(builder.Configuration);