[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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,10 @@ public class ASNote : ASObject {
|
||||||
[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"))
|
||||||
return Note.NoteVisibility.Public;
|
return Note.NoteVisibility.Public;
|
||||||
|
|
|
@ -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);
|
||||||
|
logger.LogDebug("Storing file {digest} for user {userId}", digest, user.Id);
|
||||||
var file = await db.DriveFiles.FirstOrDefaultAsync(p => p.Sha256 == digest);
|
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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue