[backend/federation] Handle note attachments (ISH-48)

This commit is contained in:
Laura Hausmann 2024-02-13 00:06:17 +01:00
parent 61c73f1761
commit 16f46dbe43
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
4 changed files with 123 additions and 8 deletions

View file

@ -0,0 +1,70 @@
using Iceshrimp.Backend.Core.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using JC = Newtonsoft.Json.JsonConverterAttribute;
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
public class ASAttachment : ASObjectBase {
[J("@type")]
[JC(typeof(StringListSingleConverter))]
public string? Type { get; set; }
}
public class ASDocument : ASAttachment {
[J("https://www.w3.org/ns/activitystreams#url")]
[JC(typeof(ASObjectBaseConverter))]
public ASObjectBase? Url { get; set; }
[J("https://www.w3.org/ns/activitystreams#mediaType")]
[JC(typeof(ValueObjectConverter))]
public string? MediaType { get; set; }
[J("https://www.w3.org/ns/activitystreams#sensitive")]
[JC(typeof(ValueObjectConverter))]
public bool? Sensitive { get; set; }
}
public sealed class ASAttachmentConverter : JsonConverter {
public override bool CanWrite => false;
public override bool CanConvert(Type objectType) {
return true;
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer) {
if (reader.TokenType == JsonToken.StartObject) {
var obj = JObject.Load(reader);
return HandleObject(obj);
}
if (reader.TokenType == JsonToken.StartArray) {
var array = JArray.Load(reader);
var result = new List<ASAttachment>();
foreach (var token in array) {
if (token is not JObject obj) return null;
var item = HandleObject(obj);
if (item == null) return null;
result.Add(item);
}
return result;
}
return null;
}
private static ASAttachment? HandleObject(JToken obj) {
var attachment = obj.ToObject<ASAttachment?>();
return attachment?.Type == $"{Constants.ActivityStreamsNs}#Document"
? obj.ToObject<ASDocument?>()
: attachment;
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}

View file

@ -47,6 +47,10 @@ public class ASNote : ASObject {
[J("https://www.w3.org/ns/activitystreams#tag")] [J("https://www.w3.org/ns/activitystreams#tag")]
[JC(typeof(ASTagConverter))] [JC(typeof(ASTagConverter))]
public List<ASTag>? Tags { get; set; } public List<ASTag>? Tags { get; set; }
[J("https://www.w3.org/ns/activitystreams#attachment")]
[JC(typeof(ASAttachmentConverter))]
public List<ASAttachment>? Attachments { get; set; }
public Note.NoteVisibility GetVisibility(ASActor actor) { public Note.NoteVisibility GetVisibility(ASActor actor) {
if (To.Any(p => p.Id == "https://www.w3.org/ns/activitystreams#Public")) if (To.Any(p => p.Id == "https://www.w3.org/ns/activitystreams#Public"))

View file

@ -17,18 +17,24 @@ public class DriveService(
IOptionsSnapshot<Config.StorageSection> storageConfig, IOptionsSnapshot<Config.StorageSection> storageConfig,
IOptions<Config.InstanceSection> instanceConfig, IOptions<Config.InstanceSection> instanceConfig,
HttpClient httpClient, HttpClient httpClient,
QueueService queueSvc QueueService queueSvc,
ILogger<DriveService> logger
) { ) {
public async Task<DriveFile?> StoreFile(string? uri, User user, bool sensitive) { public async Task<DriveFile?> StoreFile(string? uri, User user, bool sensitive) {
if (uri == null) return null; if (uri == null) return null;
logger.LogDebug("Storing file {uri} for user {userId}", uri, user.Id);
try { try {
// Do we already have the file? // Do we already have the file?
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Uri == uri); var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Uri == uri);
if (file != null) { if (file != null) {
// If the user matches, return the existing file // If the user matches, return the existing file
if (file.UserId == user.Id) if (file.UserId == user.Id) {
logger.LogDebug("File {uri} is already registered for user, returning existing file {id}",
uri, file.Id);
return file; return file;
}
// Otherwise, clone the file // Otherwise, clone the file
var req = new DriveFileCreationRequest { var req = new DriveFileCreationRequest {
@ -38,7 +44,14 @@ public class DriveService(
MimeType = null! // Not needed in .Clone MimeType = null! // Not needed in .Clone
}; };
return file.Clone(user, req); var clonedFile = file.Clone(user, req);
logger.LogDebug("File {uri} is already registered for different user, returning clone of existing file {id}, stored as {cloneId}",
uri, file.Id, clonedFile.Id);
await db.AddAsync(clonedFile);
await db.SaveChangesAsync();
return clonedFile;
} }
var res = await httpClient.GetAsync(uri); var res = await httpClient.GetAsync(uri);
@ -52,8 +65,8 @@ public class DriveService(
return await StoreFile(await res.Content.ReadAsStreamAsync(), user, request); return await StoreFile(await res.Content.ReadAsStreamAsync(), user, request);
} }
catch { catch (Exception e) {
//TODO: log error logger.LogError("Failed to insert file {uri}: {error}", uri, e.Message);
return null; return null;
} }
} }
@ -61,12 +74,20 @@ public class DriveService(
public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request) { public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request) {
var buf = new BufferedStream(data); var buf = new BufferedStream(data);
var digest = await DigestHelpers.Sha256DigestAsync(buf); var digest = await DigestHelpers.Sha256DigestAsync(buf);
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest); logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id);
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest);
if (file is { IsLink: false }) { if (file is { IsLink: false }) {
if (file.UserId == user.Id) if (file.UserId == user.Id) {
logger.LogDebug("File {digest} is already registered for user, returning existing file {id}",
digest, file.Id);
return file; return file;
}
var clonedFile = file.Clone(user, request); var clonedFile = file.Clone(user, request);
logger.LogDebug("File {digest} is already registered for different user, returning clone of existing file {id}, stored as {cloneId}",
digest, file.Id, clonedFile.Id);
await db.AddAsync(clonedFile); await db.AddAsync(clonedFile);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return clonedFile; return clonedFile;

View file

@ -33,7 +33,8 @@ public class NoteService(
NoteRenderer noteRenderer, NoteRenderer noteRenderer,
UserRenderer userRenderer, UserRenderer userRenderer,
MentionsResolver mentionsResolver, MentionsResolver mentionsResolver,
MfmConverter mfmConverter MfmConverter mfmConverter,
DriveService driveSvc
) { ) {
private readonly List<string> _resolverHistory = []; private readonly List<string> _resolverHistory = [];
private int _recursionLimit = 100; private int _recursionLimit = 100;
@ -187,6 +188,13 @@ public class NoteService(
dbNote.Text = mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, mentions, splitDomainMapping); dbNote.Text = mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, mentions, splitDomainMapping);
} }
var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null;
var files = await ProcessAttachmentsAsync(note.Attachments, user, sensitive);
if (files.Count != 0) {
dbNote.FileIds = files.Select(p => p.Id).ToList();
dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList();
}
user.NotesCount++; user.NotesCount++;
await db.Notes.AddAsync(dbNote); await db.Notes.AddAsync(dbNote);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@ -262,6 +270,18 @@ public class NoteService(
return (userIds, mentions, remoteMentions, splitDomainMapping); return (userIds, mentions, remoteMentions, splitDomainMapping);
} }
private async Task<List<DriveFile>> ProcessAttachmentsAsync(
List<ASAttachment>? attachments, User user, bool sensitive
) {
if (attachments is not { Count: > 0 }) return [];
var result = await attachments
.OfType<ASDocument>()
.Select(p => driveSvc.StoreFile(p.Url?.Id, user, p.Sensitive ?? sensitive))
.AwaitAllNoConcurrencyAsync();
return result.Where(p => p != null).Cast<DriveFile>().ToList();
}
public async Task<Note?> ResolveNoteAsync(string uri) { public async Task<Note?> ResolveNoteAsync(string uri) {
//TODO: is this enough to prevent DoS attacks? //TODO: is this enough to prevent DoS attacks?
if (_recursionLimit-- <= 0) if (_recursionLimit-- <= 0)