urlGenerator = $urlGenerator; $this->userSession = $userSession; $this->logger = $logger; $this->instanceService = $instanceService; $this->clientService = $clientService; $this->accountService = $accountService; $this->cacheActorService = $cacheActorService; $this->cacheDocumentService = $cacheDocumentService; $this->documentService = $documentService; $this->followService = $followService; $this->streamService = $streamService; $this->actionService = $actionService; $this->postService = $postService; $this->configService = $configService; $authHeader = trim($this->request->getHeader('Authorization')); if (strpos($authHeader, ' ')) { [$authType, $authToken] = explode(' ', $authHeader); if (strtolower($authType) === 'bearer') { $this->bearer = $authToken; } } } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse */ public function appsCredentials() { try { $this->initViewer(true); if ($this->client === null) { return new DataResponse( [ 'name' => 'Nextcloud Social', 'website' => 'https://github.com/nextcloud/social/' ], Http::STATUS_OK ); } else { return new DataResponse( [ 'name' => $this->client->getAppName(), 'website' => $this->client->getAppWebsite() ], Http::STATUS_OK ); } } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse */ public function verifyCredentials() { try { $this->initViewer(true); return new DataResponse($this->viewer, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse */ public function customEmojis(): DataResponse { return new DataResponse([], Http::STATUS_OK); } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse */ public function savedSearches(): DataResponse { try { $this->initViewer(true); return new DataResponse([], Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse * @throws InstanceDoesNotExistException */ public function instance(): DataResponse { $local = $this->instanceService->getLocal(Stream::FORMAT_LOCAL); return new DataResponse($local, Http::STATUS_OK); } /** * @PublicPage * @NoCSRFRequired * * @return DataResponse */ public function statusNew(): DataResponse { try { $this->initViewer(true); $input = file_get_contents('php://input'); $this->logger->debug('[ApiController] statusNew: ' . $input); $status = new Status(); $status->import($this->convertInput($input)); $post = new Post($this->accountService->getActorFromUserId($this->currentSession())); $post->setContent(nl2br($status->getStatus())); $post->setType($status->getVisibility()); if (!empty($status->getMediaIds())) { $post->setMedias( array_map(function (Document $document): MediaAttachment { return $document->convertToMediaAttachment( $this->urlGenerator, ACore::FORMAT_ACTIVITYPUB ); }, $this->documentService->getMediaFromArray( $status->getMediaIds(), $this->viewer->getPreferredUsername() )) ); } if ($status->getInReplyToId() > 0) { try { $replyTo = $this->streamService->getStreamByNid($status->getInReplyToId()); $post->setReplyTo($replyTo->getId()); } catch (StreamNotFoundException $e) { $this->logger->debug('reply to post not found'); } } $activity = $this->postService->createPost($post); $item = $this->streamService->getStreamById( $activity->getObjectId(), true, ACore::FORMAT_LOCAL ); return new DataResponse($item, Http::STATUS_OK); } catch (Exception $e) { $this->logger->warning('issues while statusNew', ['exception' => $e]); return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } } /** * @PublicPage * @NoCSRFRequired * * @return DataResponse */ public function mediaNew(): DataResponse { try { $this->initViewer(true); $file = $_FILES['file'] ?? []; if (empty($file)) { throw new Exception('no media found'); } if ($file['error'] !== UPLOAD_ERR_OK) { throw new Exception('error during upload'); } $name = $file['tmp_name'] ?? ''; $size = $file['size'] ?? -1; $type = $file['type'] ?? ''; if ($name === '' || $size === -1 || $type === '') { throw new Exception('missing details'); } $this->logger->debug('[ApiController] mediaNew: ' . json_encode($file)); $document = new Document(); $document->setLocal(true); $document->setAccount($this->viewer->getPreferredUsername()); $document->setUrlCloud($this->configService->getCloudUrl()); $document->generateUniqueId('/documents/local'); $document->setPublic(true); $this->cacheDocumentService->saveFromTempToCache($document, $name); $service = AP::$activityPub->getInterfaceForItem($document); $service->save($document); $mediaAttachment = $document->convertToMediaAttachment($this->urlGenerator); $this->logger->debug('generated attachment: ' . json_encode($mediaAttachment)); return new DataResponse($mediaAttachment, Http::STATUS_OK); } catch (Exception $e) { $this->logger->warning('issues while mediaNew', ['exception' => $e]); return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } } /** * @PublicPage * @NoCSRFRequired * * @param string $id * * @return Response */ public function mediaGet(string $nid, string $preview = ''): Response { try { return new DataResponse([], Http::STATUS_OK); } catch (Exception $e) { $this->logger->warning('issues while mediaNew', ['exception' => $e]); return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } } /** * @PublicPage * @NoCSRFRequired * * @param string $id * * @return Response */ public function mediaOpen(string $uuid): Response { $ext = ''; if (strpos($uuid, '.') > 0) { [$uuid, $ext] = explode('.', $uuid, 2); } try { $mime = ''; $file = $this->documentService->getFromUuid($uuid); return new FileDisplayResponse( $file, Http::STATUS_OK, ['Content-Type' => $this->mimeFromExt($ext)] ); } catch (Exception $e) { $this->logger->warning('issues while mediaOpen', ['exception' => $e]); return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } } /** * @param string $ext * only support image actually * * @return string */ private function mimeFromExt(string $ext): string { if ($ext === '') { return ''; } return 'image/' . $ext; } /** * @NoCSRFRequired * @PublicPage * * @param string $timeline * @param bool $local * @param int $limit * @param int $max_id * @param int $min_id * @param int $since_id * * @return DataResponse */ public function timelines( string $timeline, bool $local = false, int $limit = 20, int $max_id = 0, int $min_id = 0, int $since_id = 0, ): DataResponse { try { $this->initViewer(true); if (!in_array( strtolower($timeline), [ ProbeOptions::HOME, ProbeOptions::ACCOUNT, ProbeOptions::PUBLIC, ProbeOptions::DIRECT, ProbeOptions::FAVOURITES ] )) { throw new UnknownProbeException('unknown timeline'); } $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe($timeline) ->setLocal($local) ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since_id); $posts = $this->streamService->getTimeline($options); return new DataResponse($posts, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param int $nid * * @return DataResponse */ public function statusGet(int $nid): DataResponse { try { $this->initViewer(true); $item = $this->streamService->getStreamByNid($nid); return new DataResponse($item, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param int $nid * * @return DataResponse */ public function statusContext(int $nid): DataResponse { try { $this->initViewer(true); $context = $this->streamService->getContextByNid($nid); return new DataResponse($context, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param int $nid * @param string $action * * @return DataResponse */ public function statusAction(int $nid, string $act): DataResponse { try { $this->initViewer(true); $actor = $this->accountService->getActor($this->viewer->getPreferredUsername()); $item = $this->actionService->action($actor, $nid, $act); if ($item === null) { $item = $this->streamService->getStreamByNid($nid); } return new DataResponse($item, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param array $id * * @return DataResponse */ public function relationships(array $id): DataResponse { try { $this->initViewer(true); return new DataResponse($this->followService->getRelationships($id), Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param string $account * @param int $limit * @param int $max_id * @param int $min_id * @param int $since * * @return DataResponse */ public function accountStatuses( string $account, int $limit = 20, int $max_id = 0, int $min_id = 0, int $since_id = 0, ): DataResponse { try { $this->initViewer(true); $local = $this->cacheActorService->getFromLocalAccount($account); $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe(ProbeOptions::ACCOUNT) ->setAccountId($local->getId()) ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since_id); $posts = $this->streamService->getTimeline($options); return new DataResponse($posts, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param string $account * * @return DataResponse */ public function accountFollowing( string $account, int $limit = 20, int $max_id = 0, int $min_id = 0, int $since = 0, ): DataResponse { try { $this->initViewer(true); $local = $this->cacheActorService->getFromLocalAccount($account); $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe(ProbeOptions::FOLLOWING) ->setAccountId($local->getId()) ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since); return new DataResponse($this->cacheActorService->probeActors($options), Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param string $account * * @return DataResponse */ public function accountFollowers( string $account, int $limit = 20, int $max_id = 0, int $min_id = 0, int $since = 0, ): DataResponse { try { $this->initViewer(true); $local = $this->cacheActorService->getFromLocalAccount($account); $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe(ProbeOptions::FOLLOWERS) ->setAccountId($local->getId()) ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since); return new DataResponse($this->cacheActorService->probeActors($options), Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @param int $limit * @param int $max_id * @param int $min_id * @param int $since_id * * @return DataResponse */ public function favourites( int $limit = 20, int $max_id = 0, int $min_id = 0, int $since_id = 0, ): DataResponse { try { $this->initViewer(true); $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe(ProbeOptions::FAVOURITES) ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since_id); $posts = $this->streamService->getTimeline($options); return new DataResponse($posts, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse */ public function notifications( int $limit = 20, int $max_id = 0, int $min_id = 0, int $since_id = 0, array $types = [], array $exclude_types = [], string $accountId = '', ): DataResponse { try { $this->initViewer(true); $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe(ProbeOptions::NOTIFICATIONS) ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since_id) ->setTypes($types) ->setExcludeTypes($exclude_types) ->setAccountId($accountId); $posts = $this->streamService->getTimeline($options); return new DataResponse($posts, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * @NoCSRFRequired * @PublicPage * * @return DataResponse */ public function tag( string $hashtag, int $limit = 20, int $max_id = 0, int $min_id = 0, int $since_id = 0, bool $local = false, bool $only_media = false, ): DataResponse { try { $this->initViewer(true); $options = new ProbeOptions($this->request); $options->setFormat(ACore::FORMAT_LOCAL); $options->setProbe('hashtag') ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) ->setSince($since_id) ->setLocal($local) ->setOnlyMedia($only_media) ->setArgument($hashtag); $posts = $this->streamService->getTimeline($options); return new DataResponse($posts, Http::STATUS_OK); } catch (Exception $e) { return $this->error($e->getMessage()); } } /** * * @param bool $exception * * @return bool * @throws ClientNotFoundException */ private function initViewer(bool $exception = false): bool { try { $userId = $this->currentSession(); $this->logger->debug( '[ApiController] initViewer: ' . $userId . ' (bearer=' . $this->bearer . ')' ); $account = $this->accountService->getActorFromUserId($userId); $this->viewer = $this->cacheActorService->getFromLocalAccount($account->getPreferredUsername()); $this->viewer->setExportFormat(ACore::FORMAT_LOCAL); $this->streamService->setViewer($this->viewer); $this->followService->setViewer($this->viewer); $this->cacheActorService->setViewer($this->viewer); return true; } catch (Exception $e) { if ($exception) { throw new ClientNotFoundException('the access_token was revoked'); } } return false; } private function convertInput(string $input): array { $contentType = $this->request->getHeader('Content-Type'); $pos = strpos($contentType, ';'); if ($pos > 0) { $contentType = substr($contentType, 0, $pos); } switch ($contentType) { case 'application/json': return json_decode($input, true); case 'application/x-www-form-urlencoded': return $this->request->getParams(); default: // in case of no header ... $result = json_decode($input, true); if (is_array($result)) { return $result; } return $this->request->getParams(); } } /** * @return string * @throws AccountDoesNotExistException * @throws ClientNotFoundException */ private function currentSession(): string { $user = $this->userSession->getUser(); if ($user !== null) { return $user->getUID(); } if ($this->bearer !== '') { $this->client = $this->clientService->getFromToken($this->bearer); return $this->client->getAuthUserId(); } throw new AccountDoesNotExistException('userId not defined'); } /** * @param string $error * * @return DataResponse */ private function error(string $error): DataResponse { return new DataResponse(['error' => $error], Http::STATUS_UNAUTHORIZED); } }