diff --git a/appinfo/routes.php b/appinfo/routes.php index b3840aa9..3cbc781e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -60,6 +60,7 @@ return [ ['name' => 'OStatus#followRemote', 'url' => '/api/v1/ostatus/followRemote/{local}', 'verb' => 'GET'], ['name' => 'OStatus#getLink', 'url' => '/api/v1/ostatus/link/{local}/{account}', 'verb' => 'GET'], + // should it be moved to NavigationController ? ['name' => 'SocialPub#displayPost', 'url' => '/@{username}/{token}', 'verb' => 'GET'], ['name' => 'Local#streamHome', 'url' => '/api/v1/stream/home', 'verb' => 'GET'], @@ -72,6 +73,8 @@ return [ ['name' => 'Local#streamAccount', 'url' => '/api/v1/account/{username}/stream', 'verb' => 'GET'], ['name' => 'Local#postGet', 'url' => '/local/v1/post', 'verb' => 'GET'], + ['name' => 'Local#postReplies', 'url' => '/local/v1/post/replies', 'verb' => 'GET'], + ['name' => 'Local#postCreate', 'url' => '/api/v1/post', 'verb' => 'POST'], ['name' => 'Local#postDelete', 'url' => '/api/v1/post', 'verb' => 'DELETE'], diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php index 5f691444..bf1671e6 100644 --- a/lib/Controller/LocalController.php +++ b/lib/Controller/LocalController.php @@ -196,15 +196,15 @@ class LocalController extends Controller { * @NoAdminRequired * @NoCSRFRequired * - * @param string $postId + * @param string $id * * @return DataResponse */ - public function postGet(string $postId): DataResponse { + public function postGet(string $id): DataResponse { try { $this->initViewer(true); - $stream = $this->streamService->getStreamById($postId, true); + $stream = $this->streamService->getStreamById($id, true); return $this->directSuccess($stream); } catch (Exception $e) { @@ -213,6 +213,27 @@ class LocalController extends Controller { } + /** + * get replies about a post (limited to viewer rights). + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id + * + * @return DataResponse + */ + public function postReplies(string $id): DataResponse { + try { + $this->initViewer(true); + + return $this->success($this->streamService->getRepliesByParentId($id, true)); + } catch (Exception $e) { + return $this->fail($e); + } + } + + /** * Delete your own post. * diff --git a/lib/Db/CoreRequestBuilder.php b/lib/Db/CoreRequestBuilder.php index bfa2b1f6..e1e09a34 100644 --- a/lib/Db/CoreRequestBuilder.php +++ b/lib/Db/CoreRequestBuilder.php @@ -39,6 +39,8 @@ use OC\DB\SchemaWrapper; use OCA\Social\AP; use OCA\Social\Exceptions\DateTimeException; use OCA\Social\Exceptions\InvalidResourceException; +use OCA\Social\Exceptions\RowNotFoundException; +use OCA\Social\IQueryRow; use OCA\Social\Model\ActivityPub\Actor\Person; use OCA\Social\Model\ActivityPub\Object\Document; use OCA\Social\Model\ActivityPub\Object\Follow; @@ -198,6 +200,17 @@ class CoreRequestBuilder { } + /** + * Limit the request to the Id (string) + * + * @param IQueryBuilder $qb + * @param string $id + */ + protected function limitToInReplyTo(IQueryBuilder &$qb, string $id) { + $this->limitToDBField($qb, 'in_reply_to', $id, false); + } + + /** * Limit the request to the StreamId * @@ -1160,14 +1173,52 @@ class CoreRequestBuilder { * @param string $fieldActorId * @param string $pf */ - protected function leftJoinDetails( - IQueryBuilder $qb, string $fieldActorId = 'id', string $pf = '' - ) { + protected function leftJoinDetails(IQueryBuilder $qb, string $fieldActorId = 'id', string $pf = '') { $this->leftJoinFollowAsViewer($qb, $fieldActorId, true, 'as_follower', $pf); $this->leftJoinFollowAsViewer($qb, $fieldActorId, false, 'as_followed', $pf); } + /** + * @param IQueryBuilder $qb + * @param callable $method + * + * @return IQueryRow + * @throws RowNotFoundException + */ + public function getRowFromRequest(IQueryBuilder $qb, callable $method): IQueryRow { + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new RowNotFoundException(); + } + + return $method($data); + } + + /** + * @param IQueryBuilder $qb + * @param callable $method + * + * @return array + */ + public function getRowsFromRequest(IQueryBuilder $qb, callable $method): array { + $rows = []; + $cursor = $qb->execute(); + while ($data = $cursor->fetch()) { + try { + $rows[] = $method($data); + } catch (Exception $e) { + } + } + $cursor->closeCursor(); + + return $rows; + } + + /** * @param Person $actor * @param array $data diff --git a/lib/Db/StreamRequest.php b/lib/Db/StreamRequest.php index 20ca012e..56165c4b 100644 --- a/lib/Db/StreamRequest.php +++ b/lib/Db/StreamRequest.php @@ -171,7 +171,7 @@ class StreamRequest extends StreamRequestBuilder { * @param Document $document * @param array $attachments * - * @return array + * @return Document[] */ private function updateAttachmentInList(Document $document, array $attachments): array { $new = []; @@ -216,17 +216,7 @@ class StreamRequest extends StreamRequestBuilder { $this->limitToType($qb, $type); } - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -236,6 +226,7 @@ class StreamRequest extends StreamRequestBuilder { * * @return Stream * @throws StreamNotFoundException + * @throws SocialAppConfigException */ public function getStreamById(string $id, bool $asViewer = false): Stream { if ($id === '') { @@ -251,23 +242,41 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinStreamAction($qb); } - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $cursor->closeCursor(); - if ($data === false) { + try { + return $this->getStreamFromRequest($qb); + } catch (ItemUnknownException $e) { + throw new StreamNotFoundException('Malformed Stream'); + } catch (StreamNotFoundException $e) { throw new StreamNotFoundException( 'Stream (ById) not found - ' . $id . ' (asViewer: ' . $asViewer . ')' ); } + } - try { - $stream = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - throw new StreamNotFoundException('Malformed Stream'); + + /** + * @param string $id + * @param bool $asViewer + * + * @return Stream[] + * @throws StreamNotFoundException + */ + public function getRepliesByParentId(string $id, bool $asViewer = false): array { + if ($id === '') { + throw new StreamNotFoundException(); + }; + + $qb = $this->getStreamSelectSql(); + $this->limitToInReplyTo($qb, $id); + $this->leftJoinCacheActors($qb, 'attributed_to'); + + if ($asViewer) { + $this->limitToViewer($qb); + $this->leftJoinStreamAction($qb); } - return $stream; + return $this->getStreamsFromRequest($qb); } @@ -286,15 +295,7 @@ class StreamRequest extends StreamRequestBuilder { $qb = $this->getStreamSelectSql(); $this->limitToActivityId($qb, $id); - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $cursor->closeCursor(); - - if ($data === false) { - throw new StreamNotFoundException('Stream (ByActivityId) not found - ' . $id); - } - - return $this->parseStreamSelectSql($data); + return $this->getStreamFromRequest($qb); } @@ -319,17 +320,7 @@ class StreamRequest extends StreamRequestBuilder { $this->limitToType($qb, $type); $this->limitToSubType($qb, $subType); - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $cursor->closeCursor(); - - if ($data === false) { - throw new StreamNotFoundException( - 'StreamByObjectId not found - ' . $type . ' - ' . $objectId - ); - } - - return $this->parseStreamSelectSql($data); + return $this->getStreamFromRequest($qb); } @@ -379,17 +370,7 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinStreamAction($qb); $this->filterDuplicate($qb); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -405,7 +386,7 @@ class StreamRequest extends StreamRequestBuilder { * @param int $since * @param int $limit * - * @return array + * @return Stream[] * @throws Exception */ public function getTimelineNotifications(Person $actor, int $since = 0, int $limit = 5): array { @@ -418,17 +399,7 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinCacheActors($qb, 'attributed_to'); $this->leftJoinStreamAction($qb); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -441,7 +412,7 @@ class StreamRequest extends StreamRequestBuilder { * @param int $since * @param int $limit * - * @return array + * @return Stream[] * @throws Exception */ public function getTimelineAccount(string $actorId, int $since = 0, int $limit = 5): array { @@ -454,17 +425,7 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinCacheActors($qb, 'attributed_to'); $this->leftJoinStreamAction($qb); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -477,7 +438,7 @@ class StreamRequest extends StreamRequestBuilder { * @param int $since * @param int $limit * - * @return array + * @return Stream[] * @throws Exception */ public function getTimelineDirect(Person $actor, int $since = 0, int $limit = 5): array { @@ -492,17 +453,7 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinCacheActors($qb, 'attributed_to'); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -514,7 +465,7 @@ class StreamRequest extends StreamRequestBuilder { * @param int $limit * @param bool $localOnly * - * @return array + * @return Stream[] * @throws Exception */ public function getTimelineGlobal(int $since = 0, int $limit = 5, bool $localOnly = true @@ -531,17 +482,7 @@ class StreamRequest extends StreamRequestBuilder { // TODO: to: = real public, cc: = unlisted !? $this->limitToRecipient($qb, ACore::CONTEXT_PUBLIC, true, ['to']); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -553,7 +494,7 @@ class StreamRequest extends StreamRequestBuilder { * @param int $limit * @param bool $localOnly * - * @return array + * @return Stream[] * @throws Exception */ public function getTimelineLiked(int $since = 0, int $limit = 5, bool $localOnly = true): array { @@ -567,17 +508,7 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinActions($qb, Like::TYPE); $this->filterDBField($qb, 'id', '', false, 'a'); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - try { - $streams[] = $this->parseStreamSelectSql($data); - } catch (Exception $e) { - } - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -592,7 +523,7 @@ class StreamRequest extends StreamRequestBuilder { * @param int $since * @param int $limit * - * @return array + * @return Stream[] * @throws Exception */ public function getTimelineTag(Person $actor, string $hashtag, int $since = 0, int $limit = 5 @@ -612,14 +543,7 @@ class StreamRequest extends StreamRequestBuilder { $this->leftJoinCacheActors($qb, 'attributed_to'); $this->leftJoinStreamAction($qb); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - $streams[] = $this->parseStreamSelectSql($data); - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } @@ -628,8 +552,6 @@ class StreamRequest extends StreamRequestBuilder { * * @return Stream[] * @throws DateTimeException - * @throws ItemUnknownException - * @throws SocialAppConfigException */ public function getNoteSince(int $since): array { $qb = $this->getStreamSelectSql(); @@ -637,14 +559,7 @@ class StreamRequest extends StreamRequestBuilder { $this->limitToType($qb, Note::TYPE); $this->leftJoinStreamAction($qb); - $streams = []; - $cursor = $qb->execute(); - while ($data = $cursor->fetch()) { - $streams[] = $this->parseStreamSelectSql($data, Note::TYPE); - } - $cursor->closeCursor(); - - return $streams; + return $this->getStreamsFromRequest($qb); } diff --git a/lib/Db/StreamRequestBuilder.php b/lib/Db/StreamRequestBuilder.php index 833e6df2..7096fc7d 100644 --- a/lib/Db/StreamRequestBuilder.php +++ b/lib/Db/StreamRequestBuilder.php @@ -36,7 +36,9 @@ use Doctrine\DBAL\Query\QueryBuilder; use OCA\Social\AP; use OCA\Social\Exceptions\InvalidResourceException; use OCA\Social\Exceptions\ItemUnknownException; +use OCA\Social\Exceptions\RowNotFoundException; use OCA\Social\Exceptions\SocialAppConfigException; +use OCA\Social\Exceptions\StreamNotFoundException; use OCA\Social\Model\ActivityPub\ACore; use OCA\Social\Model\ActivityPub\Actor\Person; use OCA\Social\Model\ActivityPub\Object\Announce; @@ -447,6 +449,37 @@ class StreamRequestBuilder extends CoreRequestBuilder { } + /** + * @param IQueryBuilder $qb + * + * @return Stream + * @throws StreamNotFoundException + */ + protected function getStreamFromRequest(IQueryBuilder $qb): Stream { + /** @var Stream $result */ + try { + $result = $this->getRowFromRequest($qb, [$this, 'parseStreamSelectSql']); + } catch (RowNotFoundException $e) { + throw new StreamNotFoundException($e->getMessage()); + } + + return $result; + } + + + /** + * @param IQueryBuilder $qb + * + * @return Stream[] + */ + public function getStreamsFromRequest(IQueryBuilder $qb): array { + /** @var Stream[] $result */ + $result = $this->getRowsFromRequest($qb, [$this, 'parseStreamSelectSql']); + + return $result; + } + + /** * @param array $data * @param string $as diff --git a/lib/Exceptions/RowNotFoundException.php b/lib/Exceptions/RowNotFoundException.php new file mode 100644 index 00000000..90195abd --- /dev/null +++ b/lib/Exceptions/RowNotFoundException.php @@ -0,0 +1,39 @@ + + * @copyright 2018, Maxence Lange + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Social\Exceptions; + + +use Exception; + + +class RowNotFoundException extends Exception { + +} + diff --git a/lib/IQueryRow.php b/lib/IQueryRow.php new file mode 100644 index 00000000..1157cc02 --- /dev/null +++ b/lib/IQueryRow.php @@ -0,0 +1,57 @@ + + * @copyright 2018, Maxence Lange + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Social; + + +use OCA\Social\Exceptions\ItemAlreadyExistsException; +use OCA\Social\Exceptions\ItemNotFoundException; +use OCA\Social\Model\ActivityPub\ACore; + + +/** + * Interface IQueryRow + * + * TODO: MOVE THIS TO MyPhpTools + * + * @package OCA\Social\Service + */ +interface IQueryRow { + + + /** + * import data to feed the model. + * + * @param array $data + */ + public function import(array $data); + +} + diff --git a/lib/Model/ActivityPub/Stream.php b/lib/Model/ActivityPub/Stream.php index 2a6758db..816437e5 100644 --- a/lib/Model/ActivityPub/Stream.php +++ b/lib/Model/ActivityPub/Stream.php @@ -35,11 +35,12 @@ use daita\MySmallPhpTools\Model\CacheItem; use DateTime; use Exception; use JsonSerializable; +use OCA\Social\IQueryRow; use OCA\Social\Model\StreamAction; use OCA\Social\Traits\TDetails; -class Stream extends ACore implements JsonSerializable { +class Stream extends ACore implements IQueryRow, JsonSerializable { use TDetails; diff --git a/lib/Service/StreamService.php b/lib/Service/StreamService.php index 22b6f6a6..499af4c1 100644 --- a/lib/Service/StreamService.php +++ b/lib/Service/StreamService.php @@ -36,7 +36,6 @@ use OCA\Social\Db\StreamRequest; use OCA\Social\Exceptions\InvalidOriginException; use OCA\Social\Exceptions\InvalidResourceException; use OCA\Social\Exceptions\ItemUnknownException; -use OCA\Social\Exceptions\StreamNotFoundException; use OCA\Social\Exceptions\RedundancyLimitException; use OCA\Social\Exceptions\RequestContentException; use OCA\Social\Exceptions\RequestNetworkException; @@ -44,6 +43,7 @@ use OCA\Social\Exceptions\RequestResultNotJsonException; use OCA\Social\Exceptions\RequestResultSizeException; use OCA\Social\Exceptions\RequestServerException; use OCA\Social\Exceptions\SocialAppConfigException; +use OCA\Social\Exceptions\StreamNotFoundException; use OCA\Social\Exceptions\UnauthorizedFediverseException; use OCA\Social\Model\ActivityPub\ACore; use OCA\Social\Model\ActivityPub\Actor\Person; @@ -389,6 +389,18 @@ class StreamService { } + /** + * @param string $id + * @param bool $asViewer + * + * @return Stream[] + * @throws StreamNotFoundException + */ + public function getRepliesByParentId(string $id, bool $asViewer = false): array { + return $this->streamRequest->getRepliesByParentId($id, $asViewer); + } + + /** * @param Person $actor * @param int $since