2021-05-18 00:22:51 +02:00
< ? php
/**
2024-01-02 21:57:26 +01:00
* @ copyright Copyright ( C ) 2010 - 2024 , the Friendica project
2021-05-18 00:22:51 +02:00
*
* @ 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 < https :// www . gnu . org / licenses />.
*
*/
namespace Friendica\Module\Api\Mastodon ;
2023-01-26 00:03:51 +01:00
use Friendica\Core\Logger ;
2021-05-18 00:22:51 +02:00
use Friendica\Core\Protocol ;
use Friendica\Database\DBA ;
use Friendica\DI ;
use Friendica\Model\Contact ;
2022-11-27 00:12:46 +01:00
use Friendica\Model\Item ;
2021-05-18 00:22:51 +02:00
use Friendica\Model\Post ;
use Friendica\Model\Tag ;
use Friendica\Module\BaseApi ;
2022-11-05 23:32:56 +01:00
use Friendica\Util\Network ;
2021-05-18 00:22:51 +02:00
/**
* @ see https :// docs . joinmastodon . org / methods / search /
*/
class Search extends BaseApi
{
/**
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2021-11-20 15:38:03 +01:00
protected function rawContent ( array $request = [])
2021-05-18 00:22:51 +02:00
{
2023-10-11 15:30:42 +02:00
$this -> checkAllowedScope ( self :: SCOPE_READ );
2021-05-18 00:22:51 +02:00
$uid = self :: getCurrentUserID ();
2021-11-28 13:22:27 +01:00
$request = $this -> getRequest ([
2021-05-18 10:38:04 +02:00
'account_id' => 0 , // If provided, statuses returned will be authored only by this account
'max_id' => 0 , // Return results older than this id
'min_id' => 0 , // Return results immediately newer than this id
'type' => '' , // Enum(accounts, hashtags, statuses)
'exclude_unreviewed' => false , // Filter out unreviewed tags? Defaults to false. Use true when trying to find trending tags.
'q' => '' , // The search query
'resolve' => false , // Attempt WebFinger lookup. Defaults to false.
'limit' => 20 , // Maximum number of results to load, per type. Defaults to 20. Max 40.
2021-05-19 08:18:42 +02:00
'offset' => 0 , // Offset in search results. Used for pagination. Defaults to 0.
'following' => false , // Only include accounts that the user is following. Defaults to false.
2021-11-28 00:30:41 +01:00
], $request );
2021-07-20 22:48:37 +02:00
2021-05-18 10:38:04 +02:00
if ( empty ( $request [ 'q' ])) {
2023-10-11 15:37:49 +02:00
$this -> logAndJsonError ( 422 , $this -> errorFactory -> UnprocessableEntity ());
2021-05-18 09:01:23 +02:00
}
2021-05-18 10:38:04 +02:00
$limit = min ( $request [ 'limit' ], 40 );
2021-05-18 00:22:51 +02:00
$result = [ 'accounts' => [], 'statuses' => [], 'hashtags' => []];
2021-05-18 10:38:04 +02:00
if ( empty ( $request [ 'type' ]) || ( $request [ 'type' ] == 'accounts' )) {
$result [ 'accounts' ] = self :: searchAccounts ( $uid , $request [ 'q' ], $request [ 'resolve' ], $limit , $request [ 'offset' ], $request [ 'following' ]);
2022-11-27 00:12:46 +01:00
if ( ! is_array ( $result [ 'accounts' ])) {
// Curbing the search if we got an exact result
$request [ 'type' ] = 'accounts' ;
$result [ 'accounts' ] = [ $result [ 'accounts' ]];
}
2021-05-18 00:22:51 +02:00
}
2022-11-27 00:12:46 +01:00
2023-02-17 16:49:32 +01:00
if ( empty ( $request [ 'type' ]) || ( $request [ 'type' ] == 'statuses' )) {
2021-05-18 10:38:04 +02:00
$result [ 'statuses' ] = self :: searchStatuses ( $uid , $request [ 'q' ], $request [ 'account_id' ], $request [ 'max_id' ], $request [ 'min_id' ], $limit , $request [ 'offset' ]);
2022-11-27 00:12:46 +01:00
if ( ! is_array ( $result [ 'statuses' ])) {
// Curbing the search if we got an exact result
$request [ 'type' ] = 'statuses' ;
$result [ 'statuses' ] = [ $result [ 'statuses' ]];
}
2021-05-18 00:22:51 +02:00
}
2022-11-27 00:12:46 +01:00
2021-05-18 10:38:04 +02:00
if (( empty ( $request [ 'type' ]) || ( $request [ 'type' ] == 'hashtags' )) && ( strpos ( $request [ 'q' ], '@' ) == false )) {
2021-11-14 23:19:25 +01:00
$result [ 'hashtags' ] = self :: searchHashtags ( $request [ 'q' ], $request [ 'exclude_unreviewed' ], $limit , $request [ 'offset' ], $this -> parameters [ 'version' ]);
2021-05-18 00:22:51 +02:00
}
2023-09-21 18:16:17 +02:00
$this -> jsonExit ( $result );
2021-05-18 00:22:51 +02:00
}
2022-11-27 00:12:46 +01:00
/**
* @ param int $uid
* @ param string $q
* @ param bool $resolve
* @ param int $limit
* @ param int $offset
* @ param bool $following
* @ return array | \Friendica\Object\Api\Mastodon\Account Object if result is absolute ( exact account match ), list if not
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \Friendica\Network\HTTPException\NotFoundException
* @ throws \ImagickException
*/
2021-05-18 00:22:51 +02:00
private static function searchAccounts ( int $uid , string $q , bool $resolve , int $limit , int $offset , bool $following )
{
2023-02-15 21:13:30 +01:00
if (( $offset == 0 ) && ( strrpos ( $q , '@' ) > 0 || Network :: isValidHttpUrl ( $q ))
2022-11-27 00:12:46 +01:00
&& $id = Contact :: getIdForURL ( $q , 0 , $resolve ? null : false )
) {
return DI :: mstdnAccount () -> createFromContactId ( $id , $uid );
2022-11-05 23:08:28 +01:00
}
2021-05-18 00:22:51 +02:00
2022-11-27 00:12:46 +01:00
$accounts = [];
2023-11-24 23:06:41 +01:00
foreach ( Contact :: searchByName ( $q , '' , false , $following ? $uid : 0 , $limit , $offset ) as $contact ) {
2022-11-27 00:12:46 +01:00
$accounts [] = DI :: mstdnAccount () -> createFromContactId ( $contact [ 'id' ], $uid );
2021-05-18 00:22:51 +02:00
}
return $accounts ;
}
2022-11-27 00:12:46 +01:00
/**
* @ param int $uid
* @ param string $q
* @ param string $account_id
* @ param int $max_id
* @ param int $min_id
* @ param int $limit
* @ param int $offset
* @ return array | \Friendica\Object\Api\Mastodon\Status Object is result is absolute ( exact post match ), list if not
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \Friendica\Network\HTTPException\NotFoundException
* @ throws \ImagickException
*/
2021-05-18 00:22:51 +02:00
private static function searchStatuses ( int $uid , string $q , string $account_id , int $max_id , int $min_id , int $limit , int $offset )
{
2022-11-27 00:12:46 +01:00
if ( Network :: isValidHttpUrl ( $q )) {
$q = Network :: convertToIdn ( $q );
// If the user-specific search failed, we search and probe a public post
$item_id = Item :: fetchByLink ( $q , $uid ) ? : Item :: fetchByLink ( $q );
if ( $item_id && $item = Post :: selectFirst ([ 'uri-id' ], [ 'id' => $item_id ])) {
2023-01-25 21:14:33 +01:00
return DI :: mstdnStatus () -> createFromUriId ( $item [ 'uri-id' ], $uid , self :: appSupportsQuotes ());
2022-11-27 00:12:46 +01:00
}
}
2021-05-18 00:22:51 +02:00
$params = [ 'order' => [ 'uri-id' => true ], 'limit' => [ $offset , $limit ]];
if ( substr ( $q , 0 , 1 ) == '#' ) {
$condition = [ " `name` = ? AND (`uid` = ? OR (`uid` = ? AND NOT `global`))
AND ( `network` IN ( ? , ? , ? , ? ) OR ( `uid` = ? AND `uid` != ? )) " ,
substr ( $q , 1 ), 0 , $uid , Protocol :: ACTIVITYPUB , Protocol :: DFRN , Protocol :: DIASPORA , Protocol :: OSTATUS , $uid , 0 ];
$table = 'tag-search-view' ;
} else {
2024-01-17 20:46:22 +01:00
$q = Post\Engagement :: escapeKeywords ( $q );
2024-02-02 00:08:53 +01:00
$condition = [ " MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?)) " , $q , $uid ];
2024-01-17 20:46:22 +01:00
$table = 'post-searchindex' ;
2021-05-18 00:22:51 +02:00
}
if ( ! empty ( $max_id )) {
$condition = DBA :: mergeConditions ( $condition , [ " `uri-id` < ? " , $max_id ]);
}
if ( ! empty ( $since_id )) {
$condition = DBA :: mergeConditions ( $condition , [ " `uri-id` > ? " , $since_id ]);
}
if ( ! empty ( $min_id )) {
$condition = DBA :: mergeConditions ( $condition , [ " `uri-id` > ? " , $min_id ]);
$params [ 'order' ] = [ 'uri-id' ];
}
$items = DBA :: select ( $table , [ 'uri-id' ], $condition , $params );
2023-01-25 07:26:17 +01:00
$display_quotes = self :: appSupportsQuotes ();
2021-05-18 00:22:51 +02:00
$statuses = [];
while ( $item = Post :: fetch ( $items )) {
2021-06-16 17:02:33 +02:00
self :: setBoundaries ( $item [ 'uri-id' ]);
2023-01-26 00:03:51 +01:00
try {
$statuses [] = DI :: mstdnStatus () -> createFromUriId ( $item [ 'uri-id' ], $uid , $display_quotes );
2023-02-26 23:43:45 +01:00
} catch ( \Exception $exception ) {
Logger :: info ( 'Post not fetchable' , [ 'uri-id' => $item [ 'uri-id' ], 'uid' => $uid , 'exception' => $exception ]);
2023-01-26 00:03:51 +01:00
}
2021-05-18 00:22:51 +02:00
}
DBA :: close ( $items );
if ( ! empty ( $min_id )) {
2021-12-05 07:22:04 +01:00
$statuses = array_reverse ( $statuses );
2021-05-18 00:22:51 +02:00
}
2021-06-16 17:02:33 +02:00
self :: setLinkHeader ();
2021-05-18 00:22:51 +02:00
return $statuses ;
}
2022-11-27 00:12:46 +01:00
private static function searchHashtags ( string $q , bool $exclude_unreviewed , int $limit , int $offset , int $version ) : array
2021-05-18 00:22:51 +02:00
{
$q = ltrim ( $q , '#' );
2021-05-18 00:31:35 +02:00
2021-05-18 00:22:51 +02:00
$params = [ 'order' => [ 'name' ], 'limit' => [ $offset , $limit ]];
2022-09-24 19:56:07 +02:00
$condition = [ " `id` IN (SELECT `tid` FROM `post-tag` WHERE `type` = ?) AND `name` LIKE ? " , Tag :: HASHTAG , $q . '%' ];
2021-05-18 00:22:51 +02:00
$tags = DBA :: select ( 'tag' , [ 'name' ], $condition , $params );
$hashtags = [];
foreach ( $tags as $tag ) {
2021-07-20 22:48:37 +02:00
if ( $version == 1 ) {
$hashtags [] = $tag [ 'name' ];
} else {
$hashtags [] = new \Friendica\Object\Api\Mastodon\Tag ( DI :: baseUrl (), $tag );
}
2021-05-18 00:22:51 +02:00
}
return $hashtags ;
}
}