See https://docs.joinmastodon.org/methods/statuses/media/

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
pull/1439/head
Carl Schwan 2022-08-09 15:38:26 +02:00
rodzic e821566eca
commit 9f49b14657
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: C3AA6B3A5EFA7AC5
7 zmienionych plików z 184 dodań i 32 usunięć

Wyświetl plik

@ -81,6 +81,10 @@ return [
['name' => 'Api#savedSearches', 'url' => '/api/saved_searches/list.json', 'verb' => 'GET'],
['name' => 'Api#timelines', 'url' => '/api/v1/timelines/{timeline}/', 'verb' => 'GET'],
['name' => 'Api#notifications', 'url' => '/api/v1/notifications', 'verb' => 'GET'],
['name' => 'MediaApi#uploadMedia', 'url' => '/api/v1/media', 'verb' => 'POST'],
['name' => 'MediaApi#updateMedia', 'url' => '/api/v1/media/{id}', 'verb' => 'PUT'],
['name' => 'MediaApi#deleteMedia', 'url' => '/api/v1/media/{id}', 'verb' => 'DELETE'],
['name' => 'MediaApi#getMedia', 'url' => '/media/{shortcode}.{extension}', 'verb' => 'GET'],
// Api for local front-end
// TODO: front-end should be using the new ApiController

Wyświetl plik

@ -10,19 +10,22 @@ namespace OCA\Social\Controller;
use OCA\Social\Entity\MediaAttachment;
use OCA\Social\Service\AccountFinder;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\DB\ORM\IEntityManager;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\IMimeTypeDetector;
use OCP\Image;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Util;
use Psr\Log\LoggerInterface;
class MediaApiController extends Controller {
@ -34,6 +37,18 @@ class MediaApiController extends Controller {
private IEntityManager $entityManager;
private IURLGenerator $generator;
public const IMAGE_MIME_TYPES = [
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'image/x-xbitmap',
'image/x-ms-bmp',
'image/bmp',
'image/svg+xml',
'image/webp',
];
public function __construct(
string $appName,
IRequest $request,
@ -43,7 +58,8 @@ class MediaApiController extends Controller {
IUserSession $userSession,
AccountFinder $accountFinder,
IEntityManager $entityManager,
IURLGenerator $generator
IURLGenerator $generator,
LoggerInterface $logger
) {
parent::__construct($appName, $request);
$this->l10n = $l10n;
@ -53,6 +69,7 @@ class MediaApiController extends Controller {
$this->accountFinder = $accountFinder;
$this->entityManager = $entityManager;
$this->generator = $generator;
$this->logger = $logger;
}
/**
@ -60,7 +77,7 @@ class MediaApiController extends Controller {
*
* @NoAdminRequired
*/
public function uploadMedia(string $description, string $focus = ''): DataResponse {
public function uploadMedia(?string $description, ?string $focus = ''): DataResponse {
try {
$file = $this->getUploadedFile('file');
if (!isset($file['tmp_name'], $file['name'], $file['type'])) {
@ -90,10 +107,10 @@ class MediaApiController extends Controller {
"aspect" => $image->width() / $image->height(),
];
$attachment = new MediaAttachment();
$attachment = MediaAttachment::create();
$attachment->setMimetype($file['type']);
$attachment->setAccount($account);
$attachment->setDescription($description);
$attachment->setDescription($description ?? '');
$attachment->setMeta($meta);
$this->entityManager->persist($attachment);
$this->entityManager->flush();
@ -103,10 +120,39 @@ class MediaApiController extends Controller {
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder('media-attachments');
}
assert($attachment->getId() !== '');
$folder->newFile($attachment->getId(), $image->data());
return new DataResponse($attachment->toMastodonApi($this->generator));
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new DataResponse([
"error" => "Validation failed: File content type is invalid, File is invalid",
], 500);
}
}
/**
* @NoAdminRequired
*/
public function updateMedia(string $id, ?string $description, ?string $focus = ''): Response {
try {
$account = $this->accountFinder->getCurrentAccount($this->userSession->getUser());
$attachementRepository = $this->entityManager->getRepository(MediaAttachment::class);
$attachement = $attachementRepository->findOneBy([
'id' => $id,
]);
if ($attachement->getAccount()->getId() !== $account->getId()) {
throw new NotFoundResponse();
}
$attachement->setDescription($description ?? '');
$this->entityManager->persist($attachement);
$this->entityManager->flush();
return new DataResponse($attachement->toMastodonApi($this->generator));
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new DataResponse([
"error" => "Validation failed: File content type is invalid, File is invalid",
], 500);
@ -151,4 +197,60 @@ class MediaApiController extends Controller {
}
return $file;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function getMedia(string $shortcode, string $extension): DataDownloadResponse {
try {
$folder = $this->appData->getFolder('media-attachments');
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder('media-attachments');
}
$attachementRepository = $this->entityManager->getRepository(MediaAttachment::class);
$attachement = $attachementRepository->findOneBy([
'shortcode' => $shortcode,
]);
$file = $folder->getFile($attachement->getId());
return new DataDownloadResponse(
$file->getContent(),
(string) Http::STATUS_OK,
$this->getSecureMimeType($file->getMimeType())
);
}
/**
* @NoAdminRequired
*/
public function deleteMedia(string $id): DataResponse {
try {
$folder = $this->appData->getFolder('media-attachments');
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder('media-attachments');
}
$attachementRepository = $this->entityManager->getRepository(MediaAttachment::class);
$attachement = $attachementRepository->findOneBy([
'id' => $id,
]);
$file = $folder->getFile($attachement->getId());
$file->delete();
$this->entityManager->remove($attachement);
$this->entityManager->flush();
return new DataResponse(['removed']);
}
/**
* Allow all supported mimetypes
* Use mimetype detector for the other ones
*
* @param string $mimetype
* @return string
*/
private function getSecureMimeType(string $mimetype): string {
if (in_array($mimetype, self::IMAGE_MIME_TYPES)) {
return $mimetype;
}
return $this->mimeTypeDetector->getSecureMimeType($mimetype);
}
}

Wyświetl plik

@ -46,7 +46,7 @@ class MediaAttachment {
* @ORM\Column(type="bigint")
* @ORM\GeneratedValue
*/
private string $id = '-1';
private ?string $id = '-1';
/**
* @ORM\ManyToOne
@ -81,7 +81,7 @@ class MediaAttachment {
/**
* @ORM\Column(type="text")
*/
private ?string $description = null;
private string $description = '';
/**
* @ORM\Column
@ -101,18 +101,27 @@ class MediaAttachment {
/**
* @ORM\Column
*/
private ?string $blurhash = null;
private string $blurhash = '';
public function __construct() {
$this->updatedAt = new \DateTime();
$this->createdAt = new \DateTime();
$this->meta = [];
}
static public function create(): self {
$attachement = new MediaAttachment();
$length = 14;
$length = ($length < 4) ? 4 : $length;
$attachement->setShortcode(bin2hex(random_bytes(($length - ($length % 2)) / 2)));
return $attachement;
}
public function getId(): string {
return $this->id;
}
public function setId(string $id): void {
public function setId(?string $id): void {
$this->id = $id;
}

Wyświetl plik

@ -381,13 +381,6 @@ export default {
const formData = new FormData()
formData.append('file', event.target.files[0])
this.$store.dispatch('uploadAttachement', formData)
const previewUrl = URL.createObjectURL(event.target.files[0])
this.previewUrls.push({
description: '',
url: previewUrl,
result: event.target.files[0],
})
},
removeAttachment(idx) {
this.previewUrls.splice(idx, 1)

Wyświetl plik

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
</div>
</div>
<div class="preview-grid">
<PreviewGridItem v-for="(item, index) in miniatures" :key="index" :preview="item" :index="index" @delete="deletePreview" />
<PreviewGridItem v-for="(item, index) in draft.attachements" :key="index" :preview="item" :index="index" />
</div>
</div>
</template>
@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<script>
import PreviewGridItem from './PreviewGridItem'
import FileUpload from 'vue-material-design-icons/FileUpload'
import { mapState } from 'vuex'
export default {
name: 'PreviewGrid',
@ -34,6 +35,11 @@ export default {
PreviewGridItem,
FileUpload,
},
computed: {
...mapState({
'draft': state => state.timeline.draft,
}),
},
props: {
uploadProgress: {
type: Number,
@ -48,12 +54,6 @@ export default {
required: true,
},
},
methods: {
deletePreview(index) {
console.debug("rjeoijreo")
this.miniatures.splice(index, 1)
}
},
}
</script>

Wyświetl plik

@ -2,7 +2,7 @@
<div class="preview-item-wrapper">
<div class="preview-item" :style="backgroundStyle">
<div class="preview-item__actions">
<Button type="tertiary-no-background" @click="$emit('delete', index)">
<Button type="tertiary-no-background" @click="deletePreview">
<template #icon>
<Close :size="16" fillColor="white" />
</template>
@ -25,7 +25,7 @@
<label :for="`image-description-${index}`">
{{ t('social', 'Describe for the visually impaired') }}
</label>
<textarea :id="`image-description-${index}`" v-model="preview.description">
<textarea :id="`image-description-${index}`" v-model="internalDescription">
</textarea>
<Button type="primary" @click="closeModal">{{ t('social', 'Close') }}</Button>
</div>
@ -51,14 +51,27 @@ export default {
data() {
return {
modal: false,
internalDescription: '',
}
},
mounted() {
this.internalDescription = this.preview.description
},
methods: {
deletePreview() {
this.$store.dispatch('deleteAttachement', {
id: this.preview.id,
})
},
showModal() {
this.modal = true
},
closeModal() {
this.modal = false
this.$store.dispatch('updateAttachement', {
id: this.preview.id,
description: this.internalDescription,
})
}
},
props: {
@ -74,7 +87,7 @@ export default {
computed: {
backgroundStyle() {
return {
backgroundImage: `url("${this.preview.url}")`,
backgroundImage: `url("${this.preview.preview_url}")`,
}
},
},

Wyświetl plik

@ -53,7 +53,9 @@ const state = {
* @member {boolean}
*/
composerDisplayStatus: false,
draft: null,
draft: {
attachements: []
},
}
const mutations = {
addToTimeline(state, data) {
@ -112,7 +114,22 @@ const mutations = {
if (typeof parentAnnounce.id !== 'undefined') {
Vue.set(state.timeline[parentAnnounce.id].cache[parentAnnounce.object].object.action.values, 'boosted', false)
}
}
},
addAttachement(state, {id, description, url, preview_url}) {
state.draft.attachements.push({id, description, url, preview_url})
},
updateAttachement(state, {id, description, url, preview_url}) {
const index = state.draft.attachements.findIndex(item => {
return id === item.id
})
state.draft.attachements.splice(index, 1, {id, description, url, preview_url})
},
deleteAttachement(state, {id}) {
const index = state.draft.attachements.findIndex(item => {
return id === item.id
})
state.draft.attachements.splice(index, 1)
},
}
const getters = {
getComposerDisplayStatus(state) {
@ -146,7 +163,7 @@ const actions = {
context.commit('setAccount', account)
},
async uploadAttachement(context, formData) {
const res = await axios.post(generateUrl('apps/social/api/v1/media', formData, {
const res = await axios.post(generateUrl('apps/social/api/v1/media'), formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
@ -158,9 +175,23 @@ const actions = {
preview_url: res.data.preview_url,
})
},
async uploadAttachement() {
},
async updateAttachement(context, {id, description}) {
const res = await axios.put(generateUrl('apps/social/api/v1/media/' + id), {
description,
})
context.commit('updateAttachement', {
id: res.data.id,
description: res.data.description,
url: res.data.url,
preview_url: res.data.preview_url,
})
},
async deleteAttachement(context, {id}) {
const res = await axios.delete(generateUrl('apps/social/api/v1/media/' + id))
context.commit('deleteAttachement', {
id: res.data.id,
})
},
post(context, post) {
return new Promise((resolve, reject) => {
axios.post(generateUrl('apps/social/api/v1/post'), { data: post }).then((response) => {