diff options
-rw-r--r-- | appinfo/routes.php | 2 | ||||
-rw-r--r-- | lib/Command/Timeline.php | 11 | ||||
-rw-r--r-- | lib/Controller/ApiController.php | 58 | ||||
-rw-r--r-- | lib/Db/StreamRequest.php | 42 | ||||
-rw-r--r-- | lib/Model/Client/Options/TimelineOptions.php | 5 | ||||
-rw-r--r-- | lib/Service/StreamService.php | 33 | ||||
-rw-r--r-- | src/store/timeline.js | 82 |
7 files changed, 167 insertions, 66 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index b029b16c..a05daff8 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -83,6 +83,8 @@ return [ ['name' => 'Api#notifications', 'url' => '/api/v1/notifications', 'verb' => 'GET'], ['name' => 'Api#tag', 'url' => '/api/v1/timelines/tag/{hashtag}', 'verb' => 'GET'], ['name' => 'Api#statusNew', 'url' => '/api/v1/statuses', 'verb' => 'POST'], + ['name' => 'Api#timelines', 'url' => '/api/v1/timelines/{timeline}/', 'verb' => 'GET'], + ['name' => 'Api#accountStatuses', 'url' => '/api/v1/accounts/{account}/statuses', 'verb' => 'GET'], // Api for local front-end // TODO: front-end should be using the new ApiController diff --git a/lib/Command/Timeline.php b/lib/Command/Timeline.php index 6ab9988d..55932954 100644 --- a/lib/Command/Timeline.php +++ b/lib/Command/Timeline.php @@ -36,6 +36,7 @@ use OCA\Social\Db\StreamRequest; use OCA\Social\Model\ActivityPub\Stream; use OCA\Social\Model\Client\Options\TimelineOptions; use OCA\Social\Service\AccountService; +use OCA\Social\Service\CacheActorService; use OCA\Social\Service\ConfigService; use OCP\IUserManager; use Symfony\Component\Console\Input\InputArgument; @@ -53,6 +54,7 @@ class Timeline extends ExtendedBase { private IUserManager $userManager; private StreamRequest $streamRequest; private AccountService $accountService; + private CacheActorService $cacheActorService; private ConfigService $configService; private ?int $count = null; @@ -70,6 +72,7 @@ class Timeline extends ExtendedBase { IUserManager $userManager, StreamRequest $streamRequest, AccountService $accountService, + CacheActorService $cacheActorService, ConfigService $configService ) { parent::__construct(); @@ -77,6 +80,7 @@ class Timeline extends ExtendedBase { $this->userManager = $userManager; $this->streamRequest = $streamRequest; $this->accountService = $accountService; + $this->cacheActorService = $cacheActorService; $this->configService = $configService; } @@ -94,6 +98,7 @@ class Timeline extends ExtendedBase { ->addOption('max_id', '', InputOption::VALUE_REQUIRED, 'max_id', 0) ->addOption('since', '', InputOption::VALUE_REQUIRED, 'since', 0) ->addOption('limit', '', InputOption::VALUE_REQUIRED, 'limit', 5) + ->addOption('account', '', InputOption::VALUE_REQUIRED, 'account', '') ->addOption('crop', '', InputOption::VALUE_REQUIRED, 'crop', 0) ->setDescription('Get stream by timeline and viewer'); } @@ -136,6 +141,12 @@ class Timeline extends ExtendedBase { $options->setLocal(true); } $options->setTimeline($input->getArgument('timeline')); + + if ($input->getOption('account') !== '') { + $local = $this->cacheActorService->getFromLocalAccount($input->getOption('account')); + $options->setAccountId($local->getId()); + } + $this->outputStreams($this->streamRequest->getTimeline($options)); return 0; diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 7fbd5006..a31894bd 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -248,6 +248,7 @@ class ApiController extends Controller { * @param int $limit * @param int $max_id * @param int $min_id + * @param int $since_id * * @return DataResponse */ @@ -257,7 +258,7 @@ class ApiController extends Controller { int $limit = 20, int $max_id = 0, int $min_id = 0, - int $since = 0 + int $since_id = 0 ): DataResponse { try { $this->initViewer(true); @@ -269,6 +270,47 @@ class ApiController extends Controller { ->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 + * @param int $limit + * @param int $max_id + * @param int $min_id + * + * @return DataResponse + */ + public function accountStatuses( + 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 TimelineOptions($this->request); + $options->setFormat(ACore::FORMAT_LOCAL); + $options->setTimeline(TimelineOptions::TIMELINE_ACCOUNT) + ->setAccountId($local->getId()) + ->setLimit($limit) + ->setMaxId($max_id) + ->setMinId($min_id) ->setSince($since); $posts = $this->streamService->getTimeline($options); @@ -287,7 +329,7 @@ class ApiController extends Controller { * @param int $limit * @param int $max_id * @param int $min_id - * @param int $since + * @param int $since_id * * @return DataResponse */ @@ -295,7 +337,7 @@ class ApiController extends Controller { int $limit = 20, int $max_id = 0, int $min_id = 0, - int $since = 0 + int $since_id = 0 ): DataResponse { try { $this->initViewer(true); @@ -306,7 +348,7 @@ class ApiController extends Controller { ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) - ->setSince($since); + ->setSince($since_id); $posts = $this->streamService->getTimeline($options); @@ -327,7 +369,7 @@ class ApiController extends Controller { int $limit = 20, int $max_id = 0, int $min_id = 0, - int $since = 0, + int $since_id = 0, array $types = [], array $exclude_types = [], string $accountId = '' @@ -341,7 +383,7 @@ class ApiController extends Controller { ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) - ->setSince($since) + ->setSince($since_id) ->setTypes($types) ->setExcludeTypes($exclude_types) ->setAccountId($accountId); @@ -366,7 +408,7 @@ class ApiController extends Controller { int $limit = 20, int $max_id = 0, int $min_id = 0, - int $since = 0, + int $since_id = 0, bool $local = false, bool $only_media = false ): DataResponse { @@ -379,7 +421,7 @@ class ApiController extends Controller { ->setLimit($limit) ->setMaxId($max_id) ->setMinId($min_id) - ->setSince($since) + ->setSince($since_id) ->setLocal($local) ->setOnlyMedia($only_media) ->setArgument($hashtag); diff --git a/lib/Db/StreamRequest.php b/lib/Db/StreamRequest.php index e143fdd7..88488a19 100644 --- a/lib/Db/StreamRequest.php +++ b/lib/Db/StreamRequest.php @@ -362,6 +362,9 @@ class StreamRequest extends StreamRequestBuilder { */ public function getTimeline(TimelineOptions $options): array { switch (strtolower($options->getTimeline())) { + case TimelineOptions::TIMELINE_ACCOUNT: + $result = $this->getTimelineAccount($options); + break; case TimelineOptions::TIMELINE_HOME: $result = $this->getTimelineHome($options); break; @@ -443,6 +446,43 @@ class StreamRequest extends StreamRequestBuilder { } + + + /** + * Should returns: + * - public message from actorId. + * - followers-only if logged and follower. + * + * @param TimelineOptions $options + * + * @return Stream[] + */ + private function getTimelineAccount(TimelineOptions $options): array { + $qb = $this->getStreamSelectSql(); + + $qb->filterType(SocialAppNotification::TYPE); + $qb->paginate($options); + + $actorId = $options->getAccountId(); + if ($actorId === '') { + return []; + } + + $qb->limitToAttributedTo($actorId, true); + + $qb->selectDestFollowing('sd', ''); + $qb->innerJoinStreamDest('recipient', 'id_prim', 'sd', 's'); + $accountIsViewer = ($qb->hasViewer() && $qb->getViewer()->getId() === $actorId); + $qb->limitToDest($accountIsViewer ? '' : ACore::CONTEXT_PUBLIC, 'recipient', '', 'sd'); + + $qb->linkToCacheActors('ca', 's.attributed_to_prim'); + $qb->leftJoinStreamAction(); + + return $this->getStreamsFromRequest($qb); + } + + + /** * @param TimelineOptions $options * @@ -577,7 +617,7 @@ class StreamRequest extends StreamRequestBuilder { * @return Stream[] * @throws DateTimeException */ - public function getTimelineAccount(string $actorId, int $since = 0, int $limit = 5): array { + public function getTimelineAccount_dep(string $actorId, int $since = 0, int $limit = 5): array { $qb = $this->getStreamSelectSql(); $qb->limitPaginate($since, $limit); diff --git a/lib/Model/Client/Options/TimelineOptions.php b/lib/Model/Client/Options/TimelineOptions.php index ae844d41..fabb45a3 100644 --- a/lib/Model/Client/Options/TimelineOptions.php +++ b/lib/Model/Client/Options/TimelineOptions.php @@ -47,6 +47,7 @@ class TimelineOptions extends CoreOptions implements JsonSerializable { public const TIMELINE_HOME = 'home'; public const TIMELINE_PUBLIC = 'public'; public const TIMELINE_DIRECT = 'direct'; + public const TIMELINE_ACCOUNT = 'account'; public const TIMELINE_FAVOURITES = 'favourites'; public const TIMELINE_HASHTAG = 'hashtag'; public const TIMELINE_NOTIFICATIONS = 'notifications'; @@ -67,6 +68,7 @@ class TimelineOptions extends CoreOptions implements JsonSerializable { public static array $availableTimelines = [ self::TIMELINE_HOME, + self::TIMELINE_ACCOUNT, self::TIMELINE_PUBLIC, self::TIMELINE_DIRECT, self::TIMELINE_FAVOURITES, @@ -349,7 +351,7 @@ class TimelineOptions extends CoreOptions implements JsonSerializable { public function fromArray(array $arr): self { $this->setLocal($this->getBool('local', $arr, $this->isLocal())); $this->setRemote($this->getBool('remote', $arr, $this->isRemote())); - $this->setRemote($this->getBool('only_media', $arr, $this->isOnlyMedia())); + $this->setOnlyMedia($this->getBool('only_media', $arr, $this->isOnlyMedia())); $this->setMinId($this->getInt('min_id', $arr, $this->getMinId())); $this->setMaxId($this->getInt('max_id', $arr, $this->getMaxId())); $this->setSince($this->getInt('since', $arr, $this->getSince())); @@ -367,6 +369,7 @@ class TimelineOptions extends CoreOptions implements JsonSerializable { return [ 'timeline' => $this->getTimeline(), + 'accountId' => $this->getAccountId(), 'local' => $this->isLocal(), 'remote' => $this->isRemote(), 'only_media' => $this->isOnlyMedia(), diff --git a/lib/Service/StreamService.php b/lib/Service/StreamService.php index 46fa3904..501c6e4a 100644 --- a/lib/Service/StreamService.php +++ b/lib/Service/StreamService.php @@ -56,48 +56,29 @@ use OCA\Social\Tools\Exceptions\RequestServerException; class StreamService { private StreamRequest $streamRequest; - private ActivityService $activityService; - - private AccountService $accountService; - - private SignatureService $signatureService; - - private StreamQueueService $streamQueueService; - private CacheActorService $cacheActorService; - private ConfigService $configService; - private MiscService $miscService; - /** * NoteService constructor. * * @param StreamRequest $streamRequest * @param ActivityService $activityService - * @param AccountService $accountService - * @param SignatureService $signatureService - * @param StreamQueueService $streamQueueService * @param CacheActorService $cacheActorService * @param ConfigService $configService - * @param MiscService $miscService */ public function __construct( - StreamRequest $streamRequest, ActivityService $activityService, - AccountService $accountService, SignatureService $signatureService, - StreamQueueService $streamQueueService, CacheActorService $cacheActorService, - ConfigService $configService, MiscService $miscService + StreamRequest $streamRequest, + ActivityService $activityService, + CacheActorService $cacheActorService, + ConfigService $configService ) { $this->streamRequest = $streamRequest; $this->activityService = $activityService; - $this->accountService = $accountService; - $this->signatureService = $signatureService; - $this->streamQueueService = $streamQueueService; $this->cacheActorService = $cacheActorService; $this->configService = $configService; - $this->miscService = $miscService; } @@ -268,9 +249,7 @@ class StreamService { $note->addTag( [ 'type' => 'Hashtag', - 'href' => $this->configService->getSocialUrl() . 'tag/' . strtolower( - $hashtag - ), + 'href' => $this->configService->getSocialUrl() . 'tag/' . strtolower($hashtag), 'name' => '#' . $hashtag ] ); @@ -445,7 +424,7 @@ class StreamService { * @deprecated */ public function getStreamAccount(string $actorId, int $since = 0, int $limit = 5): array { - return $this->streamRequest->getTimelineAccount($actorId, $since, $limit); + return $this->streamRequest->getTimelineAccount_dep($actorId, $since, $limit); } diff --git a/src/store/timeline.js b/src/store/timeline.js index 0a14fe98..bd86e7eb 100644 --- a/src/store/timeline.js +++ b/src/store/timeline.js @@ -23,10 +23,13 @@ * */ -import logger from '../services/logger.js' -import axios from '@nextcloud/axios' import Vue from 'vue' + +import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' +import { showError } from '@nextcloud/dialogs' + +import logger from '../services/logger.js' /** * @property {object} timeline - The posts' collection @@ -48,8 +51,10 @@ const state = { */ params: {}, account: '', - /* Tells whether the composer should be displayed or not. + /** + * Tells whether the composer should be displayed or not. * It's up to the view to honor this status or not. + * * @member {boolean} */ composerDisplayStatus: false, @@ -153,7 +158,7 @@ const actions = { }) logger.info('Post created with token ' + data.result.token) } catch (error) { - OC.Notification.showTemporary('Failed to create a post') + showError('Failed to create a post') logger.error('Failed to create a post', { error: error.response }) } }, @@ -162,7 +167,7 @@ const actions = { context.commit('removePost', post) logger.info('Post deleted with token ' + response.data.result.token) }).catch((error) => { - OC.Notification.showTemporary('Failed to delete the post') + showError('Failed to delete the post') logger.error('Failed to delete the post', { error }) }) }, @@ -172,7 +177,7 @@ const actions = { context.commit('likePost', { post, parentAnnounce }) resolve(response) }).catch((error) => { - OC.Notification.showTemporary('Failed to like post') + showError('Failed to like post') logger.error('Failed to like post', { error: error.response }) reject(error) }) @@ -186,7 +191,7 @@ const actions = { context.commit('removePost', post) } }).catch((error) => { - OC.Notification.showTemporary('Failed to unlike post') + showError('Failed to unlike post') logger.error('Failed to unlike post', { error }) }) }, @@ -197,7 +202,7 @@ const actions = { logger.info('Post boosted with token ' + response.data.result.token) resolve(response) }).catch((error) => { - OC.Notification.showTemporary('Failed to create a boost post') + showError('Failed to create a boost post') logger.error('Failed to create a boost post', { error: error.response }) reject(error) }) @@ -208,43 +213,62 @@ const actions = { context.commit('unboostPost', { post, parentAnnounce }) logger.info('Boost deleted with token ' + response.data.result.token) }).catch((error) => { - OC.Notification.showTemporary('Failed to delete the boost') + showError('Failed to delete the boost') logger.error('Failed to delete the boost', { error }) }) }, refreshTimeline(context) { return this.dispatch('fetchTimeline', { sinceTimestamp: Math.floor(Date.now() / 1000) + 1 }) }, - fetchTimeline(context, { sinceTimestamp }) { + /** + * + * @param {object} context + * @param {object} params - see https://docs.joinmastodon.org/methods/timelines + * @param {number} [params.since_id] - Fetch results newer than ID + * @param {number} [params.max_id] - Fetch results older than ID + * @param {number} [params.min_id] - Fetch results immediately newer than ID + * @param {number} [params.limit] - Maximum number of results to return. Defaults to 20 statuses. Max 40 statuses + * @param {number} [params.local] - Show only local statuses? Defaults to false. + * @return {Promise<object>} + */ + async fetchTimeline(context, params = {}) { + if (params.since_id === undefined) { + params.since_id = state.since_id - 1 + } - if (typeof sinceTimestamp === 'undefined') { - sinceTimestamp = state.since - 1 + if (params.limit === undefined) { + params.limit = 15 } - // Compute URl to get the data + // Compute URL to get the data let url = '' - if (state.type === 'account') { - url = generateUrl(`apps/social/api/v1/account/${state.account}/stream?limit=25&since=` + sinceTimestamp) - } else if (state.type === 'tags') { - url = generateUrl(`apps/social/api/v1/stream/tag/${state.params.tag}?limit=25&since=` + sinceTimestamp) - } else if (state.type === 'single-post') { - url = generateUrl(`apps/social/local/v1/post/replies?id=${state.params.id}&limit=5&since=` + sinceTimestamp) - } else { - url = generateUrl(`apps/social/api/v1/stream/${state.type}?limit=25&since=` + sinceTimestamp) + switch (state.type) { + case 'account': + // TODO: wait for maxence + url = generateUrl(`apps/social/api/v1/timelines/${state.account}`, params) + break + case 'tags': + url = generateUrl(`apps/social/api/v1/timelines/tag/${state.params.tag}`, params) + break + case 'single-post': + // TODO: wait for maxence + url = generateUrl(`apps/social/local/v1/post/replies?id=${state.params.id}`, params) + break + default: + url = generateUrl(`apps/social/api/v1/timelines/${state.type}`, params) } // Get the data and add them to the timeline - return axios.get(url).then((response) => { + const response = await axios.get(url) - if (response.status === -1) { - throw response.message - } + if (response.status === -1) { + throw response.message + } - // Add results to timeline - context.commit('addToTimeline', response.data.result) + // Add results to timeline + context.commit('addToTimeline', response.data.result) - return response.data - }) + return response.data }, addToTimeline(context, data) { context.commit('addToTimeline', data) |