diff --git a/.htaccess-dist b/.htaccess-dist index 404137168..c5c1b5b71 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 b2e549610..b9a1dd966 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 8fa20be52..cccb233d3 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2022.12-dev (Giant Rhubarb) --- DB_UPDATE_VERSION 1491 +-- DB_UPDATE_VERSION 1492 -- ------------------------------------------ @@ -2437,6 +2437,7 @@ 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`, `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 63c487caf..2850f0ebf 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,17 @@ 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/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/) @@ -143,8 +148,6 @@ These emdpoints are planned to be implemented somewhere in the future. - [`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/Content/Widget/SavedSearches.php b/src/Content/Widget/SavedSearches.php index 6b6202ba3..1bf9e76a2 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/Model/Post.php b/src/Model/Post.php index 11e5c2c98..2baa76b5b 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 000000000..eb5e32fdc --- /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 000000000..d38899ac0 --- /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 000000000..22f8fa3f2 --- /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 000000000..f3fbad2e5 --- /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/Statuses.php b/src/Module/Api/Mastodon/Trends/Statuses.php new file mode 100644 index 000000000..8974403d7 --- /dev/null +++ b/src/Module/Api/Mastodon/Trends/Statuses.php @@ -0,0 +1,58 @@ +. + * + */ + +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 = []) + { + $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-comments' => true]]); + while ($status = Post::fetch($statuses)) { + $trending[] = DI::mstdnStatus()->createFromUriId($status['uri-id']); + } + 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 c42a27641..2014b7a38 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/Tag.php b/src/Object/Api/Mastodon/Tag.php index 340e30f70..e40b793e2 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/static/dbstructure.config.php b/static/dbstructure.config.php index 942f2237d..2f613f974 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', 1492); } return [ diff --git a/static/dbview.config.php b/static/dbview.config.php index 2452dc341..6c4f00a98 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -664,6 +664,7 @@ "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)", "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 d9e61c63f..81a9d2e2f 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' => [Module\Api\Mastodon\Trends\Tags::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/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 ]],