diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MediaController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MediaController.cs new file mode 100644 index 00000000..a01ccbb6 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/MediaController.cs @@ -0,0 +1,47 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Core.Middleware; +using Iceshrimp.Backend.Core.Services; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; + +namespace Iceshrimp.Backend.Controllers.Mastodon; + +[MastodonApiController] +[Authenticate] +[Authorize("write:media")] +[EnableCors("mastodon")] +[EnableRateLimiting("sliding")] +[Produces("application/json")] +[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Attachment))] +[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))] +[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(MastodonErrorResponse))] +public class MediaController(DriveService driveSvc) : Controller { + [HttpPost("/api/v1/media")] + [HttpPost("/api/v2/media")] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] + public async Task UploadAttachment(MediaSchemas.UploadMediaRequest request) { + var user = HttpContext.GetUserOrFail(); + var rq = new DriveFileCreationRequest { + Filename = request.File.FileName, + IsSensitive = false, + Comment = request.Description, + MimeType = request.File.ContentType, + }; + var file = await driveSvc.StoreFile(request.File.OpenReadStream(), user, rq); + var res = new Attachment { + Id = file.Id, + Type = Attachment.GetType(file.Type), + Url = file.Url, + Blurhash = file.Blurhash, + Description = file.Comment, + PreviewUrl = file.ThumbnailUrl, + RemoteUrl = file.Uri, + //Metadata = TODO + }; + + return Ok(res); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MediaSchemas.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MediaSchemas.cs new file mode 100644 index 00000000..1b5e2c76 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/MediaSchemas.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas; + +public abstract class MediaSchemas { + public class UploadMediaRequest { + [FromForm(Name = "file")] public required IFormFile File { get; set; } + [FromForm(Name = "description")] public string? Description { get; set; } + + //TODO: add thumbnail & focus properties + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs index c79c3f79..dcb1a987 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs @@ -92,8 +92,13 @@ public class StatusController(DatabaseContext db, NoteRenderer noteRenderer, Not throw GracefulException.BadRequest("Reply target is nonexistent or inaccessible") : null; - var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply); - var res = await noteRenderer.RenderAsync(note); + var attachments = request.MediaIds != null + ? await db.DriveFiles.Where(p => request.MediaIds.Contains(p.Id)).ToListAsync() + : null; + + var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply, + attachments: attachments); + var res = await noteRenderer.RenderAsync(note); return Ok(res); } diff --git a/Iceshrimp.Backend/Core/Services/DriveService.cs b/Iceshrimp.Backend/Core/Services/DriveService.cs index 11c66e11..a86acfa0 100644 --- a/Iceshrimp.Backend/Core/Services/DriveService.cs +++ b/Iceshrimp.Backend/Core/Services/DriveService.cs @@ -100,15 +100,19 @@ public class DriveService( string? blurhash = null; - if (request.MimeType.StartsWith("image/")) { + if (request.MimeType.StartsWith("image/") || request.MimeType == "image") { try { var image = await Image.LoadAsync(buf); blurhash = Blurhasher.Encode(image, 7, 7); + + // Correct mime type + if (request.MimeType == "image" && image.Metadata.DecodedImageFormat?.DefaultMimeType != null) + request.MimeType = image.Metadata.DecodedImageFormat.DefaultMimeType; } catch { logger.LogError("Failed to generate blurhash for image with mime type {type}", request.MimeType); } - + buf.Seek(0, SeekOrigin.Begin); } @@ -154,7 +158,7 @@ public class DriveService( Url = url, Name = request.Filename, Comment = request.Comment, - Type = request.MimeType, + Type = CleanMimeType(request.MimeType), RequestHeaders = request.RequestHeaders, RequestIp = request.RequestIp, Blurhash = blurhash, diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 457343d5..fb7e753b 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -39,8 +39,10 @@ public class NoteService( private readonly List _resolverHistory = []; private int _recursionLimit = 100; - public async Task CreateNoteAsync(User user, Note.NoteVisibility visibility, string? text = null, - string? cw = null, Note? reply = null, Note? renote = null) { + public async Task CreateNoteAsync( + User user, Note.NoteVisibility visibility, string? text = null, string? cw = null, Note? reply = null, + Note? renote = null, IReadOnlyCollection? attachments = null + ) { if (text?.Length > config.Value.CharacterLimit) throw GracefulException.BadRequest($"Text cannot be longer than {config.Value.CharacterLimit} characters"); @@ -52,6 +54,9 @@ public class NoteService( if (text != null) text = mentionsResolver.ResolveMentions(text, null, mentions, splitDomainMapping); + if (attachments != null && attachments.Any(p => p.UserId != user.Id)) + throw GracefulException.BadRequest("Refusing to create note with files belonging to someone else"); + var actor = await userRenderer.RenderAsync(user); var note = new Note { @@ -65,6 +70,8 @@ public class NoteService( UserHost = null, Visibility = visibility, + FileIds = attachments?.Select(p => p.Id).ToList() ?? [], + AttachedFileTypes = attachments?.Select(p => p.Type).ToList() ?? [], Mentions = mentionedUserIds, VisibleUserIds = visibility == Note.NoteVisibility.Specified ? mentionedUserIds : [], MentionedRemoteUsers = remoteMentions,