Skip to content

Commit 9f49b14

Browse files
committed
Add media api
See https://docs.joinmastodon.org/methods/statuses/media/ Signed-off-by: Carl Schwan <carl@carlschwan.eu>
1 parent e821566 commit 9f49b14

7 files changed

Lines changed: 184 additions & 32 deletions

File tree

appinfo/routes.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@
8181
['name' => 'Api#savedSearches', 'url' => '/api/saved_searches/list.json', 'verb' => 'GET'],
8282
['name' => 'Api#timelines', 'url' => '/api/v1/timelines/{timeline}/', 'verb' => 'GET'],
8383
['name' => 'Api#notifications', 'url' => '/api/v1/notifications', 'verb' => 'GET'],
84+
['name' => 'MediaApi#uploadMedia', 'url' => '/api/v1/media', 'verb' => 'POST'],
85+
['name' => 'MediaApi#updateMedia', 'url' => '/api/v1/media/{id}', 'verb' => 'PUT'],
86+
['name' => 'MediaApi#deleteMedia', 'url' => '/api/v1/media/{id}', 'verb' => 'DELETE'],
87+
['name' => 'MediaApi#getMedia', 'url' => '/media/{shortcode}.{extension}', 'verb' => 'GET'],
8488

8589
// Api for local front-end
8690
// TODO: front-end should be using the new ApiController

lib/Controller/MediaApiController.php

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@
1010
use OCA\Social\Entity\MediaAttachment;
1111
use OCA\Social\Service\AccountFinder;
1212
use OCP\AppFramework\Http;
13+
use OCP\AppFramework\Http\Response;
1314
use OCP\AppFramework\Http\DataResponse;
15+
use OCP\AppFramework\Http\DataDownloadResponse;
16+
use OCP\AppFramework\Http\NotFoundResponse;
1417
use OCP\DB\ORM\IEntityManager;
1518
use OCP\Files\IAppData;
1619
use OCP\Files\NotFoundException;
1720
use OCP\IL10N;
1821
use OCP\AppFramework\Controller;
19-
use OCP\AppFramework\Http\DataResponse;
2022
use OCP\Files\IMimeTypeDetector;
2123
use OCP\Image;
2224
use OCP\IRequest;
2325
use OCP\IURLGenerator;
2426
use OCP\IUserSession;
2527
use OCP\Util;
28+
use Psr\Log\LoggerInterface;
2629

2730
class MediaApiController extends Controller {
2831

@@ -34,6 +37,18 @@ class MediaApiController extends Controller {
3437
private IEntityManager $entityManager;
3538
private IURLGenerator $generator;
3639

40+
public const IMAGE_MIME_TYPES = [
41+
'image/png',
42+
'image/jpeg',
43+
'image/jpg',
44+
'image/gif',
45+
'image/x-xbitmap',
46+
'image/x-ms-bmp',
47+
'image/bmp',
48+
'image/svg+xml',
49+
'image/webp',
50+
];
51+
3752
public function __construct(
3853
string $appName,
3954
IRequest $request,
@@ -43,7 +58,8 @@ public function __construct(
4358
IUserSession $userSession,
4459
AccountFinder $accountFinder,
4560
IEntityManager $entityManager,
46-
IURLGenerator $generator
61+
IURLGenerator $generator,
62+
LoggerInterface $logger
4763
) {
4864
parent::__construct($appName, $request);
4965
$this->l10n = $l10n;
@@ -53,14 +69,15 @@ public function __construct(
5369
$this->accountFinder = $accountFinder;
5470
$this->entityManager = $entityManager;
5571
$this->generator = $generator;
72+
$this->logger = $logger;
5673
}
5774

5875
/**
5976
* Creates an attachment to be used with a new status.
6077
*
6178
* @NoAdminRequired
6279
*/
63-
public function uploadMedia(string $description, string $focus = ''): DataResponse {
80+
public function uploadMedia(?string $description, ?string $focus = ''): DataResponse {
6481
try {
6582
$file = $this->getUploadedFile('file');
6683
if (!isset($file['tmp_name'], $file['name'], $file['type'])) {
@@ -90,10 +107,10 @@ public function uploadMedia(string $description, string $focus = ''): DataRespon
90107
"aspect" => $image->width() / $image->height(),
91108
];
92109

93-
$attachment = new MediaAttachment();
110+
$attachment = MediaAttachment::create();
94111
$attachment->setMimetype($file['type']);
95112
$attachment->setAccount($account);
96-
$attachment->setDescription($description);
113+
$attachment->setDescription($description ?? '');
97114
$attachment->setMeta($meta);
98115
$this->entityManager->persist($attachment);
99116
$this->entityManager->flush();
@@ -103,10 +120,39 @@ public function uploadMedia(string $description, string $focus = ''): DataRespon
103120
} catch (NotFoundException $e) {
104121
$folder = $this->appData->newFolder('media-attachments');
105122
}
123+
assert($attachment->getId() !== '');
106124
$folder->newFile($attachment->getId(), $image->data());
107125

108126
return new DataResponse($attachment->toMastodonApi($this->generator));
109127
} catch (\Exception $e) {
128+
$this->logger->error($e->getMessage(), ['exception' => $e]);
129+
return new DataResponse([
130+
"error" => "Validation failed: File content type is invalid, File is invalid",
131+
], 500);
132+
}
133+
}
134+
135+
/**
136+
* @NoAdminRequired
137+
*/
138+
public function updateMedia(string $id, ?string $description, ?string $focus = ''): Response {
139+
try {
140+
$account = $this->accountFinder->getCurrentAccount($this->userSession->getUser());
141+
$attachementRepository = $this->entityManager->getRepository(MediaAttachment::class);
142+
$attachement = $attachementRepository->findOneBy([
143+
'id' => $id,
144+
]);
145+
if ($attachement->getAccount()->getId() !== $account->getId()) {
146+
throw new NotFoundResponse();
147+
}
148+
149+
$attachement->setDescription($description ?? '');
150+
$this->entityManager->persist($attachement);
151+
$this->entityManager->flush();
152+
153+
return new DataResponse($attachement->toMastodonApi($this->generator));
154+
} catch (\Exception $e) {
155+
$this->logger->error($e->getMessage(), ['exception' => $e]);
110156
return new DataResponse([
111157
"error" => "Validation failed: File content type is invalid, File is invalid",
112158
], 500);
@@ -151,4 +197,60 @@ private function getUploadedFile(string $key): array {
151197
}
152198
return $file;
153199
}
200+
201+
/**
202+
* @NoAdminRequired
203+
* @NoCSRFRequired
204+
*/
205+
public function getMedia(string $shortcode, string $extension): DataDownloadResponse {
206+
try {
207+
$folder = $this->appData->getFolder('media-attachments');
208+
} catch (NotFoundException $e) {
209+
$folder = $this->appData->newFolder('media-attachments');
210+
}
211+
$attachementRepository = $this->entityManager->getRepository(MediaAttachment::class);
212+
$attachement = $attachementRepository->findOneBy([
213+
'shortcode' => $shortcode,
214+
]);
215+
$file = $folder->getFile($attachement->getId());
216+
return new DataDownloadResponse(
217+
$file->getContent(),
218+
(string) Http::STATUS_OK,
219+
$this->getSecureMimeType($file->getMimeType())
220+
);
221+
}
222+
223+
/**
224+
* @NoAdminRequired
225+
*/
226+
public function deleteMedia(string $id): DataResponse {
227+
try {
228+
$folder = $this->appData->getFolder('media-attachments');
229+
} catch (NotFoundException $e) {
230+
$folder = $this->appData->newFolder('media-attachments');
231+
}
232+
$attachementRepository = $this->entityManager->getRepository(MediaAttachment::class);
233+
$attachement = $attachementRepository->findOneBy([
234+
'id' => $id,
235+
]);
236+
$file = $folder->getFile($attachement->getId());
237+
$file->delete();
238+
$this->entityManager->remove($attachement);
239+
$this->entityManager->flush();
240+
return new DataResponse(['removed']);
241+
}
242+
243+
/**
244+
* Allow all supported mimetypes
245+
* Use mimetype detector for the other ones
246+
*
247+
* @param string $mimetype
248+
* @return string
249+
*/
250+
private function getSecureMimeType(string $mimetype): string {
251+
if (in_array($mimetype, self::IMAGE_MIME_TYPES)) {
252+
return $mimetype;
253+
}
254+
return $this->mimeTypeDetector->getSecureMimeType($mimetype);
255+
}
154256
}

lib/Entity/MediaAttachment.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class MediaAttachment {
4646
* @ORM\Column(type="bigint")
4747
* @ORM\GeneratedValue
4848
*/
49-
private string $id = '-1';
49+
private ?string $id = '-1';
5050

5151
/**
5252
* @ORM\ManyToOne
@@ -81,7 +81,7 @@ class MediaAttachment {
8181
/**
8282
* @ORM\Column(type="text")
8383
*/
84-
private ?string $description = null;
84+
private string $description = '';
8585

8686
/**
8787
* @ORM\Column
@@ -101,18 +101,27 @@ class MediaAttachment {
101101
/**
102102
* @ORM\Column
103103
*/
104-
private ?string $blurhash = null;
104+
private string $blurhash = '';
105105

106106
public function __construct() {
107107
$this->updatedAt = new \DateTime();
108108
$this->createdAt = new \DateTime();
109+
$this->meta = [];
110+
}
111+
112+
static public function create(): self {
113+
$attachement = new MediaAttachment();
114+
$length = 14;
115+
$length = ($length < 4) ? 4 : $length;
116+
$attachement->setShortcode(bin2hex(random_bytes(($length - ($length % 2)) / 2)));
117+
return $attachement;
109118
}
110119

111120
public function getId(): string {
112121
return $this->id;
113122
}
114123

115-
public function setId(string $id): void {
124+
public function setId(?string $id): void {
116125
$this->id = $id;
117126
}
118127

src/components/Composer/Composer.vue

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -381,13 +381,6 @@ export default {
381381
const formData = new FormData()
382382
formData.append('file', event.target.files[0])
383383
this.$store.dispatch('uploadAttachement', formData)
384-
385-
const previewUrl = URL.createObjectURL(event.target.files[0])
386-
this.previewUrls.push({
387-
description: '',
388-
url: previewUrl,
389-
result: event.target.files[0],
390-
})
391384
},
392385
removeAttachment(idx) {
393386
this.previewUrls.splice(idx, 1)

src/components/Composer/PreviewGrid.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,27 @@ SPDX-License-Identifier: AGPL-3.0-or-later
1919
</div>
2020
</div>
2121
<div class="preview-grid">
22-
<PreviewGridItem v-for="(item, index) in miniatures" :key="index" :preview="item" :index="index" @delete="deletePreview" />
22+
<PreviewGridItem v-for="(item, index) in draft.attachements" :key="index" :preview="item" :index="index" />
2323
</div>
2424
</div>
2525
</template>
2626

2727
<script>
2828
import PreviewGridItem from './PreviewGridItem'
2929
import FileUpload from 'vue-material-design-icons/FileUpload'
30+
import { mapState } from 'vuex'
3031
3132
export default {
3233
name: 'PreviewGrid',
3334
components: {
3435
PreviewGridItem,
3536
FileUpload,
3637
},
38+
computed: {
39+
...mapState({
40+
'draft': state => state.timeline.draft,
41+
}),
42+
},
3743
props: {
3844
uploadProgress: {
3945
type: Number,
@@ -48,12 +54,6 @@ export default {
4854
required: true,
4955
},
5056
},
51-
methods: {
52-
deletePreview(index) {
53-
console.debug("rjeoijreo")
54-
this.miniatures.splice(index, 1)
55-
}
56-
},
5757
}
5858
</script>
5959

src/components/Composer/PreviewGridItem.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="preview-item-wrapper">
33
<div class="preview-item" :style="backgroundStyle">
44
<div class="preview-item__actions">
5-
<Button type="tertiary-no-background" @click="$emit('delete', index)">
5+
<Button type="tertiary-no-background" @click="deletePreview">
66
<template #icon>
77
<Close :size="16" fillColor="white" />
88
</template>
@@ -25,7 +25,7 @@
2525
<label :for="`image-description-${index}`">
2626
{{ t('social', 'Describe for the visually impaired') }}
2727
</label>
28-
<textarea :id="`image-description-${index}`" v-model="preview.description">
28+
<textarea :id="`image-description-${index}`" v-model="internalDescription">
2929
</textarea>
3030
<Button type="primary" @click="closeModal">{{ t('social', 'Close') }}</Button>
3131
</div>
@@ -51,14 +51,27 @@ export default {
5151
data() {
5252
return {
5353
modal: false,
54+
internalDescription: '',
5455
}
5556
},
57+
mounted() {
58+
this.internalDescription = this.preview.description
59+
},
5660
methods: {
61+
deletePreview() {
62+
this.$store.dispatch('deleteAttachement', {
63+
id: this.preview.id,
64+
})
65+
},
5766
showModal() {
5867
this.modal = true
5968
},
6069
closeModal() {
6170
this.modal = false
71+
this.$store.dispatch('updateAttachement', {
72+
id: this.preview.id,
73+
description: this.internalDescription,
74+
})
6275
}
6376
},
6477
props: {
@@ -74,7 +87,7 @@ export default {
7487
computed: {
7588
backgroundStyle() {
7689
return {
77-
backgroundImage: `url("${this.preview.url}")`,
90+
backgroundImage: `url("${this.preview.preview_url}")`,
7891
}
7992
},
8093
},

0 commit comments

Comments
 (0)