[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")]
[JC(typeof(ASTagConverter))]
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) {
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,
IOptions<Config.InstanceSection> instanceConfig,
HttpClient httpClient,
QueueService queueSvc
QueueService queueSvc,
ILogger<DriveService> logger
) {
public async Task<DriveFile?> StoreFile(string? uri, User user, bool sensitive) {
if (uri == null) return null;
logger.LogDebug("Storing file {uri} for user {userId}", uri, user.Id);
try {
// Do we already have the file?
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Uri == uri);
if (file != null) {
// 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;
}
// Otherwise, clone the file
var req = new DriveFileCreationRequest {
@ -38,7 +44,14 @@ public class DriveService(
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);
@ -52,8 +65,8 @@ public class DriveService(
return await StoreFile(await res.Content.ReadAsStreamAsync(), user, request);
}
catch {
//TODO: log error
catch (Exception e) {
logger.LogError("Failed to insert file {uri}: {error}", uri, e.Message);
return null;
}
}
@ -61,12 +74,20 @@ public class DriveService(
public async Task<DriveFile> StoreFile(Stream data, User user, DriveFileCreationRequest request) {
var buf = new BufferedStream(data);
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.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;
}
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.SaveChangesAsync();
return clonedFile;

View file

@ -33,7 +33,8 @@ public class NoteService(
NoteRenderer noteRenderer,
UserRenderer userRenderer,
MentionsResolver mentionsResolver,
MfmConverter mfmConverter
MfmConverter mfmConverter,
DriveService driveSvc
) {
private readonly List<string> _resolverHistory = [];
private int _recursionLimit = 100;
@ -187,6 +188,13 @@ public class NoteService(
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++;
await db.Notes.AddAsync(dbNote);
await db.SaveChangesAsync();
@ -262,6 +270,18 @@ public class NoteService(
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) {
//TODO: is this enough to prevent DoS attacks?
if (_recursionLimit-- <= 0)