[backend/federation] Handle note attachments (ISH-48)
This commit is contained in:
parent
61c73f1761
commit
16f46dbe43
4 changed files with 123 additions and 8 deletions
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue