Merge pull request #14103 from annando/api-channel-list
authorHypolite Petovan <hypolite@mrpetovan.com>
Fri, 19 Apr 2024 22:16:34 +0000 (18:16 -0400)
committerGitHub <noreply@github.com>
Fri, 19 Apr 2024 22:16:34 +0000 (18:16 -0400)
API: Access channels and groups via lists

12 files changed:
src/Factory/Api/Mastodon/ListEntity.php
src/Module/Api/Mastodon/Lists.php
src/Module/Api/Mastodon/Timelines/ListTimeline.php
src/Module/Conversation/Channel.php
src/Module/Conversation/Network.php
src/Module/Conversation/Timeline.php
src/Module/Ping/Network.php
src/Module/Update/Channel.php
src/Module/Update/Network.php
src/Object/Api/Mastodon/ListEntity.php
src/Object/Api/Mastodon/Status.php
static/routes.config.php

index 260ad8c..a5929dc 100644 (file)
@@ -22,6 +22,7 @@
 namespace Friendica\Factory\Api\Mastodon;
 
 use Friendica\BaseFactory;
+use Friendica\Content\Conversation\Entity\Timeline;
 use Friendica\Database\Database;
 use Friendica\Network\HTTPException\InternalServerErrorException;
 use Psr\Log\LoggerInterface;
@@ -45,4 +46,14 @@ class ListEntity extends BaseFactory
                $circle = $this->dba->selectFirst('group', ['name'], ['id' => $id, 'deleted' => false]);
                return new \Friendica\Object\Api\Mastodon\ListEntity($id, $circle['name'] ?? '', 'list');
        }
+
+       public function createFromChannel(Timeline $channel): \Friendica\Object\Api\Mastodon\ListEntity
+       {
+               return new \Friendica\Object\Api\Mastodon\ListEntity('channel:' . $channel->code, $channel->label, 'followed');
+       }
+
+       public function createFromGroup(array $group): \Friendica\Object\Api\Mastodon\ListEntity
+       {
+               return new \Friendica\Object\Api\Mastodon\ListEntity('group:' . $group['id'], $group['name'], 'followed');
+       }
 }
index fac4ed4..1260b51 100644 (file)
 
 namespace Friendica\Module\Api\Mastodon;
 
-use Friendica\Core\System;
+use Friendica\App;
+use Friendica\Core\L10n;
 use Friendica\DI;
+use Friendica\Content\Conversation\Factory\Channel as ChannelFactory;
+use Friendica\Content\Conversation\Repository;
+use Friendica\Content\GroupManager;
 use Friendica\Module\BaseApi;
 use Friendica\Model\Circle;
+use Friendica\Module\Api\ApiResponse;
+use Friendica\Util\Profiler;
+use Psr\Log\LoggerInterface;
 
 /**
  * @see https://docs.joinmastodon.org/methods/timelines/lists/
  */
 class Lists extends BaseApi
 {
+       /** @var ChannelFactory */
+       protected $channel;
+       /** @var Repository\UserDefinedChannel */
+       protected $userDefinedChannel;
+
+       public function __construct(Repository\UserDefinedChannel $userDefinedChannel, ChannelFactory $channel, \Friendica\Factory\Api\Mastodon\Error $errorFactory, App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, array $server, array $parameters = [])
+       {
+               parent::__construct($errorFactory, $app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->channel            = $channel;
+               $this->userDefinedChannel = $userDefinedChannel;
+       }
+
        protected function delete(array $request = [])
        {
                $this->checkAllowedScope(self::SCOPE_WRITE);
@@ -102,6 +122,18 @@ class Lists extends BaseApi
                        foreach (Circle::getByUserId($uid) as $circle) {
                                $lists[] = DI::mstdnList()->createFromCircleId($circle['id']);
                        }
+
+                       foreach ($this->channel->getTimelines($uid) as $channel) {
+                               $lists[] = DI::mstdnList()->createFromChannel($channel);
+                       }
+
+                       foreach ($this->userDefinedChannel->selectByUid($uid) as $channel) {
+                               $lists[] = DI::mstdnList()->createFromChannel($channel);
+                       }
+
+                       foreach (GroupManager::getList($uid, true, true, true) as $group) {
+                               $lists[] = DI::mstdnList()->createFromGroup($group);
+                       }
                } else {
                        $id = $this->parameters['id'];
 
index 552c760..ea44ee8 100644 (file)
 
 namespace Friendica\Module\Api\Mastodon\Timelines;
 
+use Friendica\App;
+use Friendica\Core\L10n;
 use Friendica\Core\Logger;
-use Friendica\Core\System;
 use Friendica\Database\DBA;
 use Friendica\DI;
+use Friendica\Model\Contact;
+use Friendica\Model\Conversation;
 use Friendica\Model\Item;
 use Friendica\Model\Post;
+use Friendica\Model\Verb;
+use Friendica\Module\Api\ApiResponse;
 use Friendica\Module\BaseApi;
+use Friendica\Module\Conversation\Timeline;
 use Friendica\Network\HTTPException;
 use Friendica\Object\Api\Mastodon\TimelineOrderByTypes;
+use Friendica\Protocol\Activity;
+use Friendica\Util\Profiler;
+use Psr\Log\LoggerInterface;
 
 /**
  * @see https://docs.joinmastodon.org/methods/timelines/
  */
 class ListTimeline extends BaseApi
 {
+       /** @var Timeline */
+       protected $timeline;
+
+       public function __construct(Timeline $timeline, \Friendica\Factory\Api\Mastodon\Error $errorFactory, App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, array $server, array $parameters = [])
+       {
+               parent::__construct($errorFactory, $app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+               $this->timeline = $timeline;
+       }
+
        /**
         * @throws HTTPException\InternalServerErrorException
         */
@@ -61,6 +79,70 @@ class ListTimeline extends BaseApi
                        'friendica_order' => TimelineOrderByTypes::ID, // Sort order options (defaults to ID)
                ], $request);
 
+               $display_quotes = self::appSupportsQuotes();
+
+               if (substr($this->parameters['id'], 0, 6) == 'group:') {
+                       $items = $this->getStatusesForGroup($uid, $request);
+               } elseif (substr($this->parameters['id'], 0, 8) == 'channel:') {
+                       $items = $this->getStatusesForChannel($uid, $request);
+               } else{
+                       $items = $this->getStatusesForCircle($uid, $request);
+               }
+
+               $statuses = [];
+               foreach ($items as $item) {
+                       try {
+                               $status =  DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes);
+                               $this->updateBoundaries($status, $item, $request['friendica_order']);
+                               $statuses[] = $status;
+                       } catch (\Throwable $th) {
+                               Logger::info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'error' => $th]);
+                       }
+               }
+
+               if (!empty($request['min_id'])) {
+                       $statuses = array_reverse($statuses);
+               }
+
+               self::setLinkHeader($request['friendica_order'] != TimelineOrderByTypes::ID);
+               $this->jsonExit($statuses);
+       }
+
+       private function getStatusesForGroup(int $uid, array $request): array
+       {
+               $cdata = Contact::getPublicAndUserContactID((int)substr($this->parameters['id'], 6), $uid);
+               $cid = $cdata['public'];
+
+               $condition = ["(`uid` = ? OR (`uid` = ? AND NOT `global`))", 0, $uid];
+
+               $condition1 = DBA::mergeConditions($condition, ["`owner-id` = ? AND `gravity` = ?", $cid, Item::GRAVITY_PARENT]);
+
+               $condition2 = DBA::mergeConditions($condition, [
+                       "`author-id` = ? AND `gravity` = ? AND `vid` = ? AND `protocol` != ? AND `thr-parent-id` = `parent-uri-id`",
+                       $cid, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), Conversation::PARCEL_DIASPORA
+               ]);
+
+               $condition1 = $this->addPagingConditions($request, $condition1);
+               $condition2 = $this->addPagingConditions($request, $condition2);
+
+               $sql1 = "SELECT `uri-id` FROM `post-thread-user-view` WHERE " . array_shift($condition1);
+               $sql2 = "SELECT `thr-parent-id` AS `uri-id` FROM `post-user-view` WHERE " . array_shift($condition2);
+
+               $condition = array_merge($condition1, $condition2);
+               $sql       = $sql1 . " UNION " . $sql2 . " GROUP BY `uri-id` " . DBA::buildParameter($this->buildOrderAndLimitParams($request));
+
+               return Post::toArray(DBA::p($sql, $condition));
+       }
+
+       private function getStatusesForChannel(int $uid, array $request): array
+       {
+               $request['friendica_order'] = TimelineOrderByTypes::ID;
+
+               return $this->timeline->getChannelItemsForAPI(substr($this->parameters['id'], 8), $uid, $request['limit'], $request['min_id'], $request['max_id']);
+       }
+
+       private function getStatusesForCircle(int $uid, array $request): array
+       {
                $condition = [
                        "`uid` = ? AND `gravity` IN (?, ?) AND `contact-id` IN (SELECT `contact-id` FROM `group_member` WHERE `gid` = ?)",
                        $uid, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT, $this->parameters['id']
@@ -89,26 +171,6 @@ class ListTimeline extends BaseApi
                }
 
                $items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params);
-
-               $display_quotes = self::appSupportsQuotes();
-
-               $statuses = [];
-               while ($item = Post::fetch($items)) {
-                       try {
-                               $status =  DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes);
-                               $this->updateBoundaries($status, $item, $request['friendica_order']);
-                               $statuses[] = $status;
-                       } catch (\Throwable $th) {
-                               Logger::info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'error' => $th]);
-                       }
-               }
-               DBA::close($items);
-
-               if (!empty($request['min_id'])) {
-                       $statuses = array_reverse($statuses);
-               }
-
-               self::setLinkHeader($request['friendica_order'] != TimelineOrderByTypes::ID);
-               $this->jsonExit($statuses);
+               return Post::toArray($items);
        }
 }
index ae16e8a..fb303ad 100644 (file)
@@ -128,7 +128,7 @@ class Channel extends Timeline
                }
 
                if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
-                       $items = $this->getChannelItems($request);
+                       $items = $this->getChannelItems($request, $this->session->getLocalUserId());
                        $order = 'created';
                } else {
                        $items = $this->getCommunityItems();
index 6f0a805..c1fc772 100644 (file)
@@ -225,7 +225,7 @@ class Network extends Timeline
 
                try {
                        if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
-                               $items = $this->getChannelItems($request);
+                               $items = $this->getChannelItems($request, $this->session->getLocalUserId());
                        } elseif ($this->community->isTimeline($this->selectedTab)) {
                                $items = $this->getCommunityItems();
                        } else {
index 9088854..6c7a090 100644 (file)
@@ -95,7 +95,7 @@ class Timeline extends BaseModule
        /** @var UserDefinedChannel */
        protected $channelRepository;
 
-       public function __construct(UserDefinedChannel $channel, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
+       public function __construct(UserDefinedChannel $channel, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server = [], array $parameters = [])
        {
                parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
 
@@ -167,7 +167,7 @@ class Timeline extends BaseModule
                                $this->maxId = $request['last_created'] ?? $this->maxId;
                                $this->minId = $request['first_created'] ?? $this->minId;
                                break;
-                       case 'uriid':
+                       case 'uri-id':
                                $this->maxId = $request['last_uriid'] ?? $this->maxId;
                                $this->minId = $request['first_uriid'] ?? $this->minId;
                                break;
@@ -229,15 +229,29 @@ class Timeline extends BaseModule
                return $tabs;
        }
 
+       public function getChannelItemsForAPI(string $channel, int $uid, int $limit, int $min = null, int $max = null): array
+       {
+               $this->itemsPerPage = $limit;
+               $this->itemUriId    = 0;
+               $this->maxId        = $max;
+               $this->minId        = $min;
+               $this->noSharer     = false;
+               $this->order        = 'uri-id';
+               $this->ping         = false;
+               $this->selectedTab  = $channel;
+
+               return $this->getChannelItems([], $uid);
+       }
+
        /**
         * Database query for the channel page
         *
         * @return array
         * @throws \Exception
         */
-       protected function getChannelItems(array $request)
+       protected function getChannelItems(array $request, int $uid): array
        {
-               $items = $this->getRawChannelItems($request);
+               $items = $this->getRawChannelItems($request, $uid);
                $total = min(count($items), $this->itemsPerPage);
 
                $contacts = $this->database->selectToArray('user-contact', ['cid'], ['channel-frequency' => Contact\User::FREQUENCY_REDUCED, 'cid' => array_column($items, 'owner-id')]);
@@ -284,14 +298,14 @@ class Timeline extends BaseModule
                                }
 
                                if (count($selected_items) < $total) {
-                                       $items = $this->getRawChannelItems($request);
+                                       $items = $this->getRawChannelItems($request, $uid);
                                }
                        }
                } else {
                        $selected_items = $items;
                }
 
-               $condition = ['unseen' => true, 'uid' => $this->session->getLocalUserId(), 'parent-uri-id' => array_column($selected_items, 'uri-id')];
+               $condition = ['unseen' => true, 'uid' => $uid, 'parent-uri-id' => array_column($selected_items, 'uri-id')];
                $this->setItemsSeenByCondition($condition);
 
                return $selected_items;
@@ -303,10 +317,8 @@ class Timeline extends BaseModule
         * @return array
         * @throws \Exception
         */
-       private function getRawChannelItems(array $request)
+       private function getRawChannelItems(array $request, int $uid): array
        {
-               $uid = $this->session->getLocalUserId();
-
                $table = 'post-engagement';
 
                if ($this->selectedTab == ChannelEntity::WHATSHOT) {
index 3326d8a..f1cecb0 100644 (file)
@@ -88,7 +88,7 @@ class Network extends NetworkModule
                $this->itemsPerPage = 100;
 
                if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
-                       $items = $this->getChannelItems($request);
+                       $items = $this->getChannelItems($request, $this->session->getLocalUserId());
                } elseif ($this->community->isTimeline($this->selectedTab)) {
                        $items = $this->getCommunityItems();
                } else {
index 0d099de..f74010e 100644 (file)
@@ -39,7 +39,7 @@ class Channel extends ChannelModule
                $o = '';
                if ($this->update || $this->force) {
                        if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
-                               $items = $this->getChannelItems($request);
+                               $items = $this->getChannelItems($request, $this->session->getLocalUserId());
                        } else {
                                $items = $this->getCommunityItems();
                        }
index bb10d53..cbda16b 100644 (file)
@@ -42,7 +42,7 @@ class Network extends NetworkModule
                }
 
                if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
-                       $items = $this->getChannelItems($request);
+                       $items = $this->getChannelItems($request, $this->session->getLocalUserId());
                } elseif ($this->community->isTimeline($this->selectedTab)) {
                        $items = $this->getCommunityItems();
                } else {
index 116f3fb..eb43db7 100644 (file)
@@ -34,6 +34,8 @@ class ListEntity extends BaseDataTransferObject
        protected $id;
        /** @var string */
        protected $title;
+       /** @var string */
+       protected $replies_policy;
 
        /**
         * Creates an list record
@@ -42,9 +44,9 @@ class ListEntity extends BaseDataTransferObject
         * @param string $title
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
-       public function __construct(int $id, string $title, string $policy)
+       public function __construct(string $id, string $title, string $policy)
        {
-               $this->id             = (string)$id;
+               $this->id             = $id;
                $this->title          = $title;
                $this->replies_policy = $policy;
        }
index 2bf9695..7d376bc 100644 (file)
@@ -160,7 +160,7 @@ class Status extends BaseDataTransferObject
 
        /**
         * Returns the current created_at string or null if not set
-        * @return \DateTime|null
+        * @return ?string
         */
        public function createdAt(): ?string
        {
index 004099e..32d328b 100644 (file)
@@ -308,7 +308,7 @@ return [
                        '/tags/{hashtag}/unfollow'           => [Module\Api\Mastodon\Tags\Unfollow::class,            [        R::POST]],
                        '/timelines/direct'                  => [Module\Api\Mastodon\Timelines\Direct::class,         [R::GET         ]],
                        '/timelines/home'                    => [Module\Api\Mastodon\Timelines\Home::class,           [R::GET         ]],
-                       '/timelines/list/{id:\d+}'           => [Module\Api\Mastodon\Timelines\ListTimeline::class,   [R::GET         ]],
+                       '/timelines/list/{id}'               => [Module\Api\Mastodon\Timelines\ListTimeline::class,   [R::GET         ]],
                        '/timelines/public'                  => [Module\Api\Mastodon\Timelines\PublicTimeline::class, [R::GET         ]],
                        '/timelines/tag/{hashtag}'           => [Module\Api\Mastodon\Timelines\Tag::class,            [R::GET         ]],
                        '/trends'                            => [Module\Api\Mastodon\Trends\Tags::class,              [R::GET         ]],