Basic note create
This commit is contained in:
parent
8d4a20896c
commit
bf3e72da9b
11 changed files with 87 additions and 40 deletions
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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); });
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
|
|
@ -14,4 +14,4 @@ public class LDIdObject() {
|
|||
}
|
||||
}
|
||||
|
||||
public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter<ASLink>;
|
||||
public sealed class LDIdObjectConverter : ASSerializer.ListSingleObjectConverter<LDIdObject>;
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"];
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue