From 7dc75d585eb3f5fb5db1263f94fbf01b37c3d805 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 10 Jul 2024 20:23:11 +0000 Subject: [PATCH] API: The media upload (audio, video) is now possible --- src/Factory/Api/Mastodon/Attachment.php | 34 +++++++++++ src/Model/Attach.php | 15 ++++- src/Model/Post/Media.php | 40 +++++++------ src/Module/Api/Mastodon/InstanceV2.php | 9 ++- src/Module/Api/Mastodon/Media.php | 56 +++++++++++++++---- src/Module/Api/Mastodon/Statuses.php | 30 ++++++++-- src/Module/Media/Attachment/Upload.php | 2 +- .../InstanceV2/MediaAttachmentsConfig.php | 4 +- static/routes.config.php | 2 +- 9 files changed, 151 insertions(+), 41 deletions(-) diff --git a/src/Factory/Api/Mastodon/Attachment.php b/src/Factory/Api/Mastodon/Attachment.php index 18207fa1be..f60ed89377 100644 --- a/src/Factory/Api/Mastodon/Attachment.php +++ b/src/Factory/Api/Mastodon/Attachment.php @@ -23,6 +23,7 @@ namespace Friendica\Factory\Api\Mastodon; use Friendica\App\BaseURL; use Friendica\BaseFactory; +use Friendica\Model\Attach; use Friendica\Model\Photo; use Friendica\Network\HTTPException; use Friendica\Model\Post; @@ -144,4 +145,37 @@ class Attachment extends BaseFactory $object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, 'image', $url, $preview_url, ''); return $object->toArray(); } + + /** + * @param int $id id of the attachment + * + * @return array + * @throws HTTPException\InternalServerErrorException + */ + public function createFromAttach(int $id): array + { + $media = Attach::selectFirst(['id', 'filetype'], ['id' => $id]); + if (empty($media)) { + return []; + } + $attachment = [ + 'id' => 'attach:' . $media['id'], + 'description' => null, + 'blurhash' => null, + ]; + + $types = [Post\Media::AUDIO => 'audio', Post\Media::VIDEO => 'video', Post\Media::IMAGE => 'image']; + + $type = Post\Media::getType($media['filetype']); + + $url = $this->baseUrl . '/attach/' . $id; + + $object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, $types[$type] ?? 'unknown', $url, '', ''); + return $object->toArray(); + } + + public function isAttach(string $id): bool + { + return substr($id, 0, 7) == 'attach:'; + } } diff --git a/src/Model/Attach.php b/src/Model/Attach.php index 9b7962f0b1..c245def1e2 100644 --- a/src/Model/Attach.php +++ b/src/Model/Attach.php @@ -245,6 +245,7 @@ class Attach * @param string $src Source file name * @param int $uid User id * @param string $filename Optional file name + * @param string $filetype Optional file type * @param string $allow_cid * @param string $allow_gid * @param string $deny_cid @@ -252,7 +253,7 @@ class Attach * @return boolean|int Insert id or false on failure * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function storeFile(string $src, int $uid, string $filename = '', string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', string $deny_gid = '') + public static function storeFile(string $src, int $uid, string $filename = '', string $filetype = '', string $allow_cid = '', string $allow_gid = '', string $deny_cid = '', string $deny_gid = '') { if ($filename === '') { $filename = basename($src); @@ -260,7 +261,7 @@ class Attach $data = @file_get_contents($src); - return self::store($data, $uid, $filename, '', null, $allow_cid, $allow_gid, $deny_cid, $deny_gid); + return self::store($data, $uid, $filename, $filetype, null, $allow_cid, $allow_gid, $deny_cid, $deny_gid); } @@ -345,6 +346,16 @@ class Attach } } + public static function setPermissionForId(int $id, int $uid, string $str_contact_allow, string $str_circle_allow, string $str_contact_deny, string $str_circle_deny) + { + $fields = [ + 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, + 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny, + ]; + + self::update($fields, ['id' => $id, 'uid' => $uid]); + } + public static function addAttachmentToBody(string $body, int $uid): string { preg_match_all("/\[attachment\](.*?)\[\/attachment\]/ism", $body, $matches, PREG_SET_ORDER); diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index 7159591821..7f71cc32ff 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -444,42 +444,46 @@ class Media return $data; } - $type = explode('/', current(explode(';', $data['mimetype']))); + $data['type'] = self::getType($data['mimetype']); + return $data; + } + + public static function getType(string $mimeType): int + { + $type = explode('/', current(explode(';', $mimeType))); if (count($type) < 2) { - Logger::info('Unknown MimeType', ['type' => $type, 'media' => $data]); - $data['type'] = self::UNKNOWN; - return $data; + Logger::info('Unknown MimeType', ['type' => $type, 'media' => $mimeType]); + return self::UNKNOWN; } $filetype = strtolower($type[0]); $subtype = strtolower($type[1]); if ($filetype == 'image') { - $data['type'] = self::IMAGE; + $type = self::IMAGE; } elseif ($filetype == 'video') { - $data['type'] = self::VIDEO; + $type = self::VIDEO; } elseif ($filetype == 'audio') { - $data['type'] = self::AUDIO; + $type = self::AUDIO; } elseif (($filetype == 'text') && ($subtype == 'html')) { - $data['type'] = self::HTML; + $type = self::HTML; } elseif (($filetype == 'text') && ($subtype == 'xml')) { - $data['type'] = self::XML; + $type = self::XML; } elseif (($filetype == 'text') && ($subtype == 'plain')) { - $data['type'] = self::PLAIN; + $type = self::PLAIN; } elseif ($filetype == 'text') { - $data['type'] = self::TEXT; + $type = self::TEXT; } elseif (($filetype == 'application') && ($subtype == 'x-bittorrent')) { - $data['type'] = self::TORRENT; + $type = self::TORRENT; } elseif ($filetype == 'application') { - $data['type'] = self::APPLICATION; + $type = self::APPLICATION; } else { - $data['type'] = self::UNKNOWN; - Logger::info('Unknown type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $data]); - return $data; + $type = self::UNKNOWN; + Logger::info('Unknown type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $mimeType]); } - Logger::debug('Detected type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $data]); - return $data; + Logger::debug('Detected type', ['filetype' => $filetype, 'subtype' => $subtype, 'media' => $mimeType]); + return $type; } /** diff --git a/src/Module/Api/Mastodon/InstanceV2.php b/src/Module/Api/Mastodon/InstanceV2.php index 137a2d31c2..f247a7a021 100644 --- a/src/Module/Api/Mastodon/InstanceV2.php +++ b/src/Module/Api/Mastodon/InstanceV2.php @@ -131,12 +131,19 @@ class InstanceV2 extends BaseApi return new InstanceEntity\Configuration( $statuses_config, - new InstanceEntity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit), + new InstanceEntity\MediaAttachmentsConfig($this->supportedMimeTypes(), $image_size_limit, $image_matrix_limit), new InstanceEntity\Polls(), new InstanceEntity\Accounts(), ); } + private function supportedMimeTypes(): array + { + $mimetypes = ['audio/aac', 'audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/wav', + 'audio/webm', 'video/mp4', 'video/ogg', 'video/webm']; + return array_merge(Images::supportedMimeTypes(), $mimetypes); + } + private function buildContactInfo(): InstanceEntity\Contact { $email = implode(',', User::getAdminEmailList()); diff --git a/src/Module/Api/Mastodon/Media.php b/src/Module/Api/Mastodon/Media.php index 529b5c83fb..4e762fa87a 100644 --- a/src/Module/Api/Mastodon/Media.php +++ b/src/Module/Api/Mastodon/Media.php @@ -22,8 +22,9 @@ namespace Friendica\Module\Api\Mastodon; use Friendica\Core\Logger; -use Friendica\Core\System; use Friendica\DI; +use Friendica\Model\Attach; +use Friendica\Model\Contact; use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Module\BaseApi; @@ -51,14 +52,38 @@ class Media extends BaseApi $this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity()); } - $media = Photo::upload($uid, $_FILES['file'], '', null, null, '', '', $request['description']); - if (empty($media)) { - $this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity()); + $type = Post\Media::getType($_FILES['file']['type']); + + if (in_array($type, [Post\Media::IMAGE, Post\Media::UNKNOWN])) { + $media = Photo::upload($uid, $_FILES['file'], '', null, null, '', '', $request['description']); + if (empty($media)) { + $this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity()); + } + + Logger::info('Uploaded photo', ['media' => $media]); + + $this->jsonExit(DI::mstdnAttachment()->createFromPhoto($media['id'])); + } else { + $tempFileName = $_FILES['file']['tmp_name']; + $fileName = basename($_FILES['file']['name']); + $fileSize = intval($_FILES['file']['size']); + $maxFileSize = DI::config()->get('system', 'maxfilesize'); + + if ($fileSize <= 0) { + @unlink($tempFileName); + $this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity()); + } + + if ($maxFileSize && $fileSize > $maxFileSize) { + @unlink($tempFileName); + $this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity()); + } + + $id = Attach::storeFile($tempFileName, self::getCurrentUserID(), $fileName, $_FILES['file']['type'], '<' . Contact::getPublicIdByUserId(self::getCurrentUserID()) . '>'); + @unlink($tempFileName); + Logger::info('Uploaded media', ['id' => $id]); + $this->jsonExit(DI::mstdnAttachment()->createFromAttach($id)); } - - Logger::info('Uploaded photo', ['media' => $media]); - - $this->jsonExit(DI::mstdnAttachment()->createFromPhoto($media['id'])); } public function put(array $request = []) @@ -77,6 +102,10 @@ class Media extends BaseApi $this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity()); } + if (DI::mstdnAttachment()->isAttach($this->parameters['id']) && Attach::exists(['id' => substr($this->parameters['id'], 7)])) { + $this->jsonExit(DI::mstdnAttachment()->createFromAttach(substr($this->parameters['id'], 7))); + } + $photo = Photo::selectFirst(['resource-id'], ['id' => $this->parameters['id'], 'uid' => $uid]); if (empty($photo['resource-id'])) { $media = Post\Media::getById($this->parameters['id']); @@ -108,10 +137,15 @@ class Media extends BaseApi } $id = $this->parameters['id']; - if (!Photo::exists(['id' => $id, 'uid' => $uid])) { - $this->logAndJsonError(404, $this->errorFactory->RecordNotFound()); + + if (Photo::exists(['id' => $id, 'uid' => $uid])) { + $this->jsonExit(DI::mstdnAttachment()->createFromPhoto($id)); } - $this->jsonExit(DI::mstdnAttachment()->createFromPhoto($id)); + if (DI::mstdnAttachment()->isAttach($id) && Attach::exists(['id' => substr($id, 7)])) { + $this->jsonExit(DI::mstdnAttachment()->createFromAttach(substr($id, 7))); + } + + $this->logAndJsonError(404, $this->errorFactory->RecordNotFound()); } } diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 2c5e706e47..a9e2baa114 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -28,6 +28,7 @@ use Friendica\Core\Protocol; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Model\Attach; use Friendica\Model\Contact; use Friendica\Model\Circle; use Friendica\Model\Item; @@ -397,6 +398,20 @@ class Statuses extends BaseApi $item['attachments'] = []; foreach ($media_ids as $id) { + if (DI::mstdnAttachment()->isAttach($id) && Attach::exists(['id' => substr($id, 7)])) { + $attach = Attach::selectFirst([], ['id' => substr($id, 7)]); + $attachment = [ + 'type' => Post\Media::getType($attach['filetype']), + 'mimetype' => $attach['filetype'], + 'url' => DI::baseUrl() . '/attach/' . substr($id, 7), + 'size' => $attach['filetype'], + 'name' => $attach['filename'] + ]; + $item['attachments'][] = $attachment; + Attach::setPermissionForId(substr($id, 7), $item['uid'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']); + continue; + } + $media = DBA::toArray(DBA::p("SELECT `resource-id`, `scale`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo` WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = ?) AND `photo`.`uid` = ? ORDER BY `photo`.`width` DESC LIMIT 2", $id, $item['uid'])); @@ -409,13 +424,16 @@ class Statuses extends BaseApi $ext = Images::getExtensionByMimeType($media[0]['type']); - $attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'], - 'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . $ext, - 'size' => $media[0]['datasize'], - 'name' => $media[0]['filename'] ?: $media[0]['resource-id'], + $attachment = [ + 'type' => Post\Media::IMAGE, + 'mimetype' => $media[0]['type'], + 'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . $ext, + 'size' => $media[0]['datasize'], + 'name' => $media[0]['filename'] ?: $media[0]['resource-id'], 'description' => $media[0]['desc'] ?? '', - 'width' => $media[0]['width'], - 'height' => $media[0]['height']]; + 'width' => $media[0]['width'], + 'height' => $media[0]['height'] + ]; if (count($media) > 1) { $attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . $ext; diff --git a/src/Module/Media/Attachment/Upload.php b/src/Module/Media/Attachment/Upload.php index 042c216290..f257fb245c 100644 --- a/src/Module/Media/Attachment/Upload.php +++ b/src/Module/Media/Attachment/Upload.php @@ -106,7 +106,7 @@ class Upload extends \Friendica\BaseModule $this->return(401, $msg); } - $newid = Attach::storeFile($tempFileName, $owner['uid'], $fileName, '<' . $owner['id'] . '>'); + $newid = Attach::storeFile($tempFileName, $_FILES['userfile']['type'] ?? '', $owner['uid'], $fileName, '<' . $owner['id'] . '>'); @unlink($tempFileName); diff --git a/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php b/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php index cfdb363424..07a47f7df6 100644 --- a/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php +++ b/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php @@ -39,7 +39,7 @@ class MediaAttachmentsConfig extends BaseDataTransferObject /** @var int */ protected $video_size_limit = 0; /** @var int */ - protected $video_frame_rate_limit = 0; + protected $video_frame_rate_limit = 60; /** @var int */ protected $video_matrix_limit = 0; @@ -51,5 +51,7 @@ class MediaAttachmentsConfig extends BaseDataTransferObject $this->supported_mime_types = $supported_mime_types; $this->image_size_limit = $image_size_limit; $this->image_matrix_limit = $image_matrix_limit; + $this->video_size_limit = $image_size_limit; + $this->video_matrix_limit = $image_matrix_limit; } } diff --git a/static/routes.config.php b/static/routes.config.php index e9065e863e..cec1b9eded 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -261,7 +261,7 @@ return [ '/lists/{id:\d+}' => [Module\Api\Mastodon\Lists::class, [R::GET, R::PUT, R::DELETE]], '/lists/{id:\d+}/accounts' => [Module\Api\Mastodon\Lists\Accounts::class, [R::GET, R::POST, R::DELETE]], '/markers' => [Module\Api\Mastodon\Markers::class, [R::GET, R::POST]], - '/media/{id:\d+}' => [Module\Api\Mastodon\Media::class, [R::GET, R::PUT ]], + '/media/{id}' => [Module\Api\Mastodon\Media::class, [R::GET, R::PUT ]], '/mutes' => [Module\Api\Mastodon\Mutes::class, [R::GET ]], '/notifications' => [Module\Api\Mastodon\Notifications::class, [R::GET ]], '/notifications/{id:\d+}' => [Module\Api\Mastodon\Notifications::class, [R::GET ]],