diff --git a/.htaccess-dist b/.htaccess-dist index 404137168a..c5c1b5b716 100644 --- a/.htaccess-dist +++ b/.htaccess-dist @@ -51,6 +51,6 @@ AddType audio/ogg .oga RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^(.*)$ index.php?pagename=$1 [E=REMOTE_USER:%{HTTP:Authorization},L,QSA] + RewriteRule ^(.*)$ index.php?pagename=$1 [E=REMOTE_USER:%{HTTP:Authorization},L,QSA,B] diff --git a/CHANGELOG b/CHANGELOG index b2e549610f..b9a1dd966b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ Version 2022.12 (unreleased) Friendica Core + The rewrite rule in .htaccess-dist has been changed. The change has to be applied manually to the existing .htaccess Friendica Addons BREAKING: The functions from the boot.php file have been moved into better fitting classes diff --git a/database.sql b/database.sql index 8fa20be52c..04401f4c0d 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2022.12-dev (Giant Rhubarb) --- DB_UPDATE_VERSION 1491 +-- DB_UPDATE_VERSION 1493 -- ------------------------------------------ @@ -2437,6 +2437,8 @@ CREATE VIEW `post-thread-view` AS SELECT `post-question`.`end-time` AS `question-end-time`, 0 AS `has-categories`, EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-thread`.`uri-id`) AS `has-media`, + (SELECT COUNT(*) FROM `post` WHERE `parent-uri-id` = `post-thread`.`uri-id` AND `gravity` = 6) AS `total-comments`, + (SELECT COUNT(DISTINCT(`author-id`)) FROM `post` WHERE `parent-uri-id` = `post-thread`.`uri-id` AND `gravity` = 6) AS `total-actors`, `diaspora-interaction`.`interaction` AS `signed_text`, `parent-item-uri`.`guid` AS `parent-guid`, `parent-post`.`network` AS `parent-network`, diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 63c487caf3..0e2f574303 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -73,7 +73,7 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - `:id` is a follow request ID, not a regular account id - Returns a [Relationship](https://docs.joinmastodon.org/entities/relationship) object. - +- [`GET /api/v1/followed_tags'](https://docs.joinmastodon.org/methods/followed_tags/) - [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance#fetch-instance) - `GET /api/v1/instance/rules` Undocumented, returns Terms of Service - [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains) @@ -126,12 +126,18 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`GET /api/v1/statuses/:id/source`](https://docs.joinmastodon.org/methods/statuses/#source) - [`GET /api/v1/statuses/:id/card`](https://docs.joinmastodon.org/methods/statuses/#card) - [`GET /api/v1/suggestions`](https://docs.joinmastodon.org/methods/accounts/suggestions/) +- [`GET /api/v1/tags/:id`](https://docs.joinmastodon.org/methods/tags/#get) +- [`GET /api/v1/tags/:id/follow`](https://docs.joinmastodon.org/methods/tags/#follow) +- [`GET /api/v1/tags/:id/unfollow`](https://docs.joinmastodon.org/methods/tags/#unfollow) - [`GET /api/v1/timelines/direct`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/timelines/home`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/timelines/list/:id`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/timelines/public`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/timelines/tag/:hashtag`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/trends`](https://docs.joinmastodon.org/methods/instance/trends/) +- [`GET /api/v1/trends/links`](https://github.com/mastodon/mastodon/pull/16917) +- [`GET /api/v1/trends/statuses`](https://docs.joinmastodon.org/methods/trends/#statuses) +- [`GET /api/v1/trends/tags`](https://docs.joinmastodon.org/methods/trends/#tags) - [`GET /api/v2/search`](https://docs.joinmastodon.org/methods/search/) @@ -142,9 +148,6 @@ These emdpoints are planned to be implemented somewhere in the future. - [`POST /api/v1/accounts/:id/remove_from_followers`](https://github.com/mastodon/mastodon/pull/16864) - [`GET /api/v1/accounts/familiar_followers`](https://github.com/mastodon/mastodon/pull/17700) - [`GET /api/v1/accounts/lookup`](https://github.com/mastodon/mastodon/pull/15740) -- [`GET /api/v1/trends/links`](https://github.com/mastodon/mastodon/pull/16917) -- [`GET /api/v1/trends/statuses`](https://github.com/mastodon/mastodon/pull/17431) -- [`GET /api/v1/trends/tags`](https://github.com/mastodon/mastodon/pull/16917) - [`POST /api/v1/polls/:id/votes`](https://docs.joinmastodon.org/methods/statuses/polls/) - [`GET /api/v1/featured_tags`](https://docs.joinmastodon.org/methods/accounts/featured_tags/) - [`POST /api/v1/featured_tags`](https://docs.joinmastodon.org/methods/accounts/featured_tags/) diff --git a/src/App/Router.php b/src/App/Router.php index 35ea9ada8b..5b915c0e6a 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -80,7 +80,7 @@ class Router /** * @var array Module parameters */ - private $parameters = []; + protected $parameters = []; /** @var L10n */ private $l10n; @@ -268,7 +268,6 @@ class Router * * @throws InternalServerErrorException * @throws MethodNotAllowedException - * @throws NotFoundException */ public function getModuleClass(): string { @@ -284,9 +283,8 @@ class Router * * @return void * - * @throws HTTPException\InternalServerErrorException - * @throws HTTPException\MethodNotAllowedException If a rule matched but the method didn't - * @throws HTTPException\NotFoundException If no rule matched + * @throws HTTPException\InternalServerErrorException Unexpected exceptions + * @throws HTTPException\MethodNotAllowedException If a rule is private only */ private function determineModuleClass(): void { @@ -295,39 +293,26 @@ class Router $dispatcher = new FriendicaGroupCountBased($this->getCachedDispatchData()); - $this->parameters = []; + $this->parameters = [$this->server]; - // Check if the HTTP method is OPTIONS and return the special Options Module with the possible HTTP methods - if ($this->args->getMethod() === static::OPTIONS) { - $this->moduleClass = Options::class; - $this->parameters = ['allowedMethods' => $dispatcher->getOptions($cmd)]; - } else { - $routeInfo = $dispatcher->dispatch($this->args->getMethod(), $cmd); - if ($routeInfo[0] === Dispatcher::FOUND) { - $this->moduleClass = $routeInfo[1]; - $this->parameters = $routeInfo[2]; - } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { - throw new HTTPException\MethodNotAllowedException($this->l10n->t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); - } else { - throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); - } - } - } - - public function getModule(?string $module_class = null): ICanHandleRequests - { - $module_parameters = [$this->server]; - /** - * ROUTING - * - * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the - * post() and/or content() static methods can be respectively called to produce a data change or an output. - **/ try { - $module_class = $module_class ?? $this->getModuleClass(); - $module_parameters[] = $this->parameters; + // Check if the HTTP method is OPTIONS and return the special Options Module with the possible HTTP methods + if ($this->args->getMethod() === static::OPTIONS) { + $this->moduleClass = Options::class; + $this->parameters = ['allowedMethods' => $dispatcher->getOptions($cmd)]; + } else { + $routeInfo = $dispatcher->dispatch($this->args->getMethod(), $cmd); + if ($routeInfo[0] === Dispatcher::FOUND) { + $this->moduleClass = $routeInfo[1]; + $this->parameters[] = $routeInfo[2]; + } else if ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { + throw new HTTPException\MethodNotAllowedException($this->l10n->t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); + } else { + throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); + } + } } catch (MethodNotAllowedException $e) { - $module_class = MethodNotAllowed::class; + $this->moduleClass = MethodNotAllowed::class; } catch (NotFoundException $e) { $moduleName = $this->args->getModuleName(); // Then we try addon-provided modules that we wrap in the LegacyModule class @@ -339,8 +324,8 @@ class Router } else { include_once "addon/{$moduleName}/{$moduleName}.php"; if (function_exists($moduleName . '_module')) { - $module_parameters[] = "addon/{$moduleName}/{$moduleName}.php"; - $module_class = LegacyModule::class; + $this->parameters[] = "addon/{$moduleName}/{$moduleName}.php"; + $this->moduleClass = LegacyModule::class; } } } @@ -348,24 +333,29 @@ class Router /* Finally, we look for a 'standard' program module in the 'mod' directory * We emulate a Module class through the LegacyModule class */ - if (!$module_class && file_exists("mod/{$moduleName}.php")) { - $module_parameters[] = "mod/{$moduleName}.php"; - $module_class = LegacyModule::class; + if (!$this->moduleClass && file_exists("mod/{$moduleName}.php")) { + $this->parameters[] = "mod/{$moduleName}.php"; + $this->moduleClass = LegacyModule::class; } - $module_class = $module_class ?: PageNotFound::class; + $this->moduleClass = $this->moduleClass ?: PageNotFound::class; } + } + + public function getModule(?string $module_class = null): ICanHandleRequests + { + $moduleClass = $module_class ?? $this->getModuleClass(); $stamp = microtime(true); try { /** @var ICanHandleRequests $module */ - return $this->dice->create($module_class, $module_parameters); + return $this->dice->create($moduleClass, $this->parameters); } finally { if ($this->dice_profiler_threshold > 0) { $dur = floatval(microtime(true) - $stamp); if ($dur >= $this->dice_profiler_threshold) { - $this->logger->notice('Dice module creation lasts too long.', ['duration' => round($dur, 3), 'module' => $module_class, 'parameters' => $module_parameters]); + $this->logger->notice('Dice module creation lasts too long.', ['duration' => round($dur, 3), 'module' => $moduleClass, 'parameters' => $this->parameters]); } } } diff --git a/src/Content/Widget/SavedSearches.php b/src/Content/Widget/SavedSearches.php index 6b6202ba32..1bf9e76a26 100644 --- a/src/Content/Widget/SavedSearches.php +++ b/src/Content/Widget/SavedSearches.php @@ -37,7 +37,7 @@ class SavedSearches public static function getHTML(string $return_url, string $search = ''): string { $saved = []; - $saved_searches = DBA::select('search', ['id', 'term'], ['uid' => DI::userSession()->getLocalUserId()]); + $saved_searches = DBA::select('search', ['id', 'term'], ['uid' => DI::userSession()->getLocalUserId()], ['order' => ['term']]); while ($saved_search = DBA::fetch($saved_searches)) { $saved[] = [ 'id' => $saved_search['id'], diff --git a/src/Factory/Api/Mastodon/Card.php b/src/Factory/Api/Mastodon/Card.php index 659ab3a3de..ac50841847 100644 --- a/src/Factory/Api/Mastodon/Card.php +++ b/src/Factory/Api/Mastodon/Card.php @@ -30,13 +30,14 @@ use Friendica\Util\Strings; class Card extends BaseFactory { /** - * @param int $uriId Uri-ID of the item + * @param int $uriId Uri-ID of the item + * @param array $history Link request history * * @return \Friendica\Object\Api\Mastodon\Card * @throws HTTPException\InternalServerErrorException * @throws \ImagickException*@throws \Exception */ - public function createFromUriId(int $uriId): \Friendica\Object\Api\Mastodon\Card + public function createFromUriId(int $uriId, array $history = []): \Friendica\Object\Api\Mastodon\Card { $item = Post::selectFirst(['body'], ['uri-id' => $uriId]); if (!empty($item['body'])) { @@ -76,6 +77,6 @@ class Card extends BaseFactory } } - return new \Friendica\Object\Api\Mastodon\Card($data); + return new \Friendica\Object\Api\Mastodon\Card($data, $history); } } diff --git a/src/Model/Post.php b/src/Model/Post.php index 11e5c2c985..2baa76b5be 100644 --- a/src/Model/Post.php +++ b/src/Model/Post.php @@ -374,6 +374,21 @@ class Post return self::selectView('post-thread-user-view', $selected, $condition, $params); } + /** + * Select rows from the post-thread-view view + * + * @param array $selected Array of selected fields, empty for all + * @param array $condition Array of fields for condition + * @param array $params Array of several parameters + * + * @return boolean|object + * @throws \Exception + */ + public static function selectPostThread(array $selected = [], array $condition = [], array $params = []) + { + return self::selectView('post-thread-view', $selected, $condition, $params); + } + /** * Select rows from the given view for a given user * diff --git a/src/Module/Api/Mastodon/FollowedTags.php b/src/Module/Api/Mastodon/FollowedTags.php new file mode 100644 index 0000000000..eb5e32fdc1 --- /dev/null +++ b/src/Module/Api/Mastodon/FollowedTags.php @@ -0,0 +1,83 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/followed_tags/ + */ +class FollowedTags extends BaseApi +{ + protected function rawContent(array $request = []) + { + self::checkAllowedScope(self::SCOPE_READ); + $uid = self::getCurrentUserID(); + + $request = $this->getRequest([ + 'max_id' => 0, + 'since_id' => 0, + 'min_id' => 0, + 'limit' => 100, // Maximum number of results to return. Defaults to 100. Paginate using the HTTP Link header. + ], $request); + + $params = ['order' => ['id' => true], 'limit' => $request['limit']]; + + $condition = ["`uid` = ? AND `term` LIKE ?", $uid, '#%']; + + if (!empty($request['max_id'])) { + $condition = DBA::mergeConditions($condition, ["`id` < ?", $request['max_id']]); + } + + if (!empty($request['since_id'])) { + $condition = DBA::mergeConditions($condition, ["`id` > ?", $request['since_id']]); + } + + if (!empty($request['min_id'])) { + $condition = DBA::mergeConditions($condition, ["`id` > ?", $request['min_id']]); + + $params['order'] = ['id']; + } + + $return = []; + + $saved_searches = DBA::select('search', ['id', 'term'], $condition); + while ($saved_search = DBA::fetch($saved_searches)) { + self::setBoundaries($saved_search['id']); + $tag = ['name' => ltrim($saved_search['term'], '#')]; + + $hashtag = new \Friendica\Object\Api\Mastodon\Tag($this->baseUrl, $tag, [], true); + $return[] = $hashtag->toArray(); + } + + DBA::close($saved_searches); + + if (!empty($request['min_id'])) { + $return = array_reverse($return); + } + + self::setLinkHeader(); + System::jsonExit($return); + } +} diff --git a/src/Module/Api/Mastodon/Tags.php b/src/Module/Api/Mastodon/Tags.php new file mode 100644 index 0000000000..d38899ac0f --- /dev/null +++ b/src/Module/Api/Mastodon/Tags.php @@ -0,0 +1,53 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/tags/ + */ +class Tags extends BaseApi +{ + /** + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + protected function rawContent(array $request = []) + { + self::checkAllowedScope(self::SCOPE_READ); + $uid = self::getCurrentUserID(); + + if (empty($this->parameters['hashtag'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + $tag = ltrim($this->parameters['hashtag'], '#'); + $following = DBA::exists('search', ['uid' => $uid, 'term' => '#' . $tag]); + $term = ['term' => $tag]; + + $hashtag = new \Friendica\Object\Api\Mastodon\Tag($this->baseUrl, $term, [], $following); + System::jsonExit($hashtag->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Tags/Follow.php b/src/Module/Api/Mastodon/Tags/Follow.php new file mode 100644 index 0000000000..22f8fa3f27 --- /dev/null +++ b/src/Module/Api/Mastodon/Tags/Follow.php @@ -0,0 +1,51 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Tags; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/tags/#follow + */ +class Follow extends BaseApi +{ + protected function post(array $request = []) + { + self::checkAllowedScope(self::SCOPE_WRITE); + $uid = self::getCurrentUserID(); + + if (empty($this->parameters['hashtag'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + $fields = ['uid' => $uid, 'term' => '#' . ltrim($this->parameters['hashtag'], '#')]; + if (!DBA::exists('search', $fields)) { + DBA::insert('search', $fields); + } + + $hashtag = new \Friendica\Object\Api\Mastodon\Tag($this->baseUrl, $fields, [], true); + System::jsonExit($hashtag->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Tags/Unfollow.php b/src/Module/Api/Mastodon/Tags/Unfollow.php new file mode 100644 index 0000000000..f3fbad2e5a --- /dev/null +++ b/src/Module/Api/Mastodon/Tags/Unfollow.php @@ -0,0 +1,50 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Tags; + +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/tags/#unfollow + */ +class Unfollow extends BaseApi +{ + protected function post(array $request = []) + { + self::checkAllowedScope(self::SCOPE_WRITE); + $uid = self::getCurrentUserID(); + + if (empty($this->parameters['hashtag'])) { + DI::mstdnError()->UnprocessableEntity(); + } + + $term = ['uid' => $uid, 'term' => '#' . ltrim($this->parameters['hashtag'], '#')]; + + DBA::delete('search', $term); + + $hashtag = new \Friendica\Object\Api\Mastodon\Tag($this->baseUrl, $term, [], false); + System::jsonExit($hashtag->toArray()); + } +} diff --git a/src/Module/Api/Mastodon/Trends/Links.php b/src/Module/Api/Mastodon/Trends/Links.php new file mode 100644 index 0000000000..b41b1d6eaa --- /dev/null +++ b/src/Module/Api/Mastodon/Trends/Links.php @@ -0,0 +1,60 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Trends; + +use Friendica\Core\Protocol; +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Model\Post; +use Friendica\Module\BaseApi; +use Friendica\Util\DateTimeFormat; + +/** + * @see https://docs.joinmastodon.org/methods/trends/#links + */ +class Links extends BaseApi +{ + /** + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + protected function rawContent(array $request = []) + { + $request = $this->getRequest([ + 'limit' => 10, // Maximum number of results to return. Defaults to 10. + ], $request); + + $condition = ["EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-thread-view`.`uri-id` AND `type` = ? AND NOT `name` IS NULL AND NOT `description` IS NULL) AND NOT `private` AND `commented` > ? AND `created` > ?", + Post\Media::HTML, DateTimeFormat::utc('now -1 day'), DateTimeFormat::utc('now -1 week')]; + $condition = DBA::mergeConditions($condition, ['network' => Protocol::FEDERATED]); + + $trending = []; + $statuses = Post::selectPostThread(['uri-id', 'total-comments', 'total-actors'], $condition, ['limit' => $request['limit'], 'order' => ['total-actors' => true]]); + while ($status = Post::fetch($statuses)) { + $history = [['day' => (string)time(), 'uses' => (string)$status['total-comments'], 'accounts' => (string)$status['total-actors']]]; + $trending[] = DI::mstdnCard()->createFromUriId($status['uri-id'], $history)->toArray(); + } + DBA::close($statuses); + + System::jsonExit($trending); + } +} diff --git a/src/Module/Api/Mastodon/Trends/Statuses.php b/src/Module/Api/Mastodon/Trends/Statuses.php new file mode 100644 index 0000000000..26eb52d7ae --- /dev/null +++ b/src/Module/Api/Mastodon/Trends/Statuses.php @@ -0,0 +1,60 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon\Trends; + +use Friendica\Core\Protocol; +use Friendica\Core\System; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Model\Post; +use Friendica\Module\BaseApi; +use Friendica\Util\DateTimeFormat; + +/** + * @see https://docs.joinmastodon.org/methods/trends/#statuses + */ +class Statuses extends BaseApi +{ + /** + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + protected function rawContent(array $request = []) + { + $uid = self::getCurrentUserID(); + + $request = $this->getRequest([ + 'limit' => 10, // Maximum number of results to return. Defaults to 10. + ], $request); + + $condition = ["NOT `private` AND `commented` > ? AND `created` > ?", DateTimeFormat::utc('now -1 day'), DateTimeFormat::utc('now -1 week')]; + $condition = DBA::mergeConditions($condition, ['network' => Protocol::FEDERATED]); + + $trending = []; + $statuses = Post::selectPostThread(['uri-id'], $condition, ['limit' => $request['limit'], 'order' => ['total-actors' => true]]); + while ($status = Post::fetch($statuses)) { + $trending[] = DI::mstdnStatus()->createFromUriId($status['uri-id'], $uid); + } + DBA::close($statuses); + + System::jsonExit($trending); + } +} diff --git a/src/Module/Api/Mastodon/Trends.php b/src/Module/Api/Mastodon/Trends/Tags.php similarity index 95% rename from src/Module/Api/Mastodon/Trends.php rename to src/Module/Api/Mastodon/Trends/Tags.php index c42a27641b..2014b7a384 100644 --- a/src/Module/Api/Mastodon/Trends.php +++ b/src/Module/Api/Mastodon/Trends/Tags.php @@ -19,7 +19,7 @@ * */ -namespace Friendica\Module\Api\Mastodon; +namespace Friendica\Module\Api\Mastodon\Trends; use Friendica\Core\System; use Friendica\DI; @@ -29,7 +29,7 @@ use Friendica\Module\BaseApi; /** * @see https://docs.joinmastodon.org/methods/instance/trends/ */ -class Trends extends BaseApi +class Tags extends BaseApi { /** * @throws \Friendica\Network\HTTPException\InternalServerErrorException diff --git a/src/Object/Api/Mastodon/Card.php b/src/Object/Api/Mastodon/Card.php index 4bd6c6e769..bf87617190 100644 --- a/src/Object/Api/Mastodon/Card.php +++ b/src/Object/Api/Mastodon/Card.php @@ -59,7 +59,7 @@ class Card extends BaseDataTransferObject * @param array $attachment Attachment record * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(array $attachment) + public function __construct(array $attachment, array $history = []) { $this->url = $attachment['url'] ?? ''; $this->title = $attachment['title'] ?? ''; @@ -72,6 +72,7 @@ class Card extends BaseDataTransferObject $this->width = $attachment['width'] ?? 0; $this->height = $attachment['height'] ?? 0; $this->image = $attachment['image'] ?? ''; + $this->history = $history; } /** diff --git a/src/Object/Api/Mastodon/Tag.php b/src/Object/Api/Mastodon/Tag.php index 340e30f703..e40b793e26 100644 --- a/src/Object/Api/Mastodon/Tag.php +++ b/src/Object/Api/Mastodon/Tag.php @@ -37,18 +37,23 @@ class Tag extends BaseDataTransferObject protected $url = null; /** @var array */ protected $history = []; + /** @var bool */ + protected $following = false; /** * Creates a hashtag record from an tag-view record. * * @param BaseURL $baseUrl - * @param array $tag tag-view record + * @param array $tag tag-view record + * @param array $history + * @param array $following "true" if the user is following this tag * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(BaseURL $baseUrl, array $tag, array $history = []) + public function __construct(BaseURL $baseUrl, array $tag, array $history = [], bool $following = false) { - $this->name = strtolower($tag['name']); - $this->url = $baseUrl . '/search?tag=' . urlencode($this->name); - $this->history = $history; + $this->name = strtolower($tag['name']); + $this->url = $baseUrl . '/search?tag=' . urlencode($this->name); + $this->history = $history; + $this->following = $following; } } diff --git a/src/Object/Post.php b/src/Object/Post.php index 8b8463f858..c81b49580f 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -622,7 +622,7 @@ class Post public function addChild(Post $item) { if (!$item->getId()) { - Logger::fatal('Post object has no id', ['post' => $item]); + Logger::error('Post object has no id', ['post' => $item]); return false; } elseif ($this->getChild($item->getId())) { Logger::warning('Post object already exists', ['post' => $item]); @@ -633,7 +633,7 @@ class Post * Only add what will be displayed */ if ($item->getDataValue('network') === Protocol::MAIL && DI::userSession()->getLocalUserId() != $item->getDataValue('uid')) { - Logger::warning('Post object does not belong to local user', ['post' => $item, 'local_user' => $local_user]); + Logger::warning('Post object does not belong to local user', ['post' => $item, 'local_user' => DI::userSession()->getLocalUserId()]); return false; } elseif (DI::activity()->match($item->getDataValue('verb'), Activity::LIKE) || DI::activity()->match($item->getDataValue('verb'), Activity::DISLIKE)) { diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 942f2237d5..4f231b4c32 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1491); + define('DB_UPDATE_VERSION', 1493); } return [ diff --git a/static/dbview.config.php b/static/dbview.config.php index 2452dc341b..d06904a91f 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -664,6 +664,8 @@ "question-end-time" => ["post-question", "end-time"], "has-categories" => "0", "has-media" => "EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-thread`.`uri-id`)", + "total-comments" => "(SELECT COUNT(*) FROM `post` WHERE `parent-uri-id` = `post-thread`.`uri-id` AND `gravity` = 6)", + "total-actors" => "(SELECT COUNT(DISTINCT(`author-id`)) FROM `post` WHERE `parent-uri-id` = `post-thread`.`uri-id` AND `gravity` = 6)", "signed_text" => ["diaspora-interaction", "interaction"], "parent-guid" => ["parent-item-uri", "guid"], "parent-network" => ["parent-post", "network"], diff --git a/static/routes.config.php b/static/routes.config.php index d9e61c63fc..da5dda0278 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -233,10 +233,10 @@ return [ '/featured_tags' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST]], // not supported '/featured_tags/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::DELETE ]], // not supported '/featured_tags/suggestions' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not supported - '/filters' => [Module\Api\Mastodon\Filters::class, [R::GET ]], // Dummy, not supported '/filters/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST, R::PUT, R::DELETE]], // not supported '/follow_requests' => [Module\Api\Mastodon\FollowRequests::class, [R::GET ]], '/follow_requests/{id:\d+}/{action}' => [Module\Api\Mastodon\FollowRequests::class, [ R::POST]], + '/followed_tags' => [Module\Api\Mastodon\FollowedTags::class, [R::GET ]], '/instance' => [Module\Api\Mastodon\Instance::class, [R::GET ]], '/instance/activity' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo '/instance/peers' => [Module\Api\Mastodon\Instance\Peers::class, [R::GET ]], @@ -287,18 +287,22 @@ return [ '/streaming/user' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/streaming/user/notification' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented '/suggestions/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::DELETE ]], // not implemented + '/tags/{hashtag}' => [Module\Api\Mastodon\Tags::class, [R::GET ]], + '/tags/{hashtag}/follow' => [Module\Api\Mastodon\Tags\Follow::class, [ R::POST]], + '/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/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::class, [R::GET ]], - '/trends/links' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented - '/trends/statuses' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented - '/trends/tags' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not implemented + '/trends' => [Module\Api\Mastodon\Trends\Tags::class, [R::GET ]], + '/trends/links' => [Module\Api\Mastodon\Trends\Links::class, [R::GET ]], + '/trends/statuses' => [Module\Api\Mastodon\Trends\Statuses::class, [R::GET ]], + '/trends/tags' => [Module\Api\Mastodon\Trends\Tags::class, [R::GET ]], ], '/v{version:\d+}' => [ '/admin/accounts' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not supported + '/filters' => [Module\Api\Mastodon\Filters::class, [R::GET ]], // Dummy, not supported '/media' => [Module\Api\Mastodon\Media::class, [ R::POST]], '/search' => [Module\Api\Mastodon\Search::class, [R::GET ]], '/suggestions' => [Module\Api\Mastodon\Suggestions::class, [R::GET ]],