pixelfed/pixelfed

View on GitHub
app/Services/SearchApiV2Service.php

Summary

Maintainability
F
4 days
Test Coverage
<?php

namespace App\Services;

use App\Hashtag;
use App\Profile;
use App\Status;
use App\Transformer\Api\AccountTransformer;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;


class SearchApiV2Service
{
    private $query;

    public static $mastodonMode = false;

    public static function query($query, $mastodonMode = false)
    {
        self::$mastodonMode = $mastodonMode;

        return (new self)->run($query);
    }

    protected function run($query)
    {
        $this->query = $query;
        $q = urldecode($query->input('q'));

        if ($query->has('resolve') &&
            (Str::startsWith($q, 'https://') ||
              Str::substrCount($q, '@') >= 1)
        ) {
            return $this->resolveQuery();
        }

        if ($query->has('type')) {
            switch ($query->input('type')) {
                case 'accounts':
                    return [
                        'accounts' => $this->accounts(),
                        'hashtags' => [],
                        'statuses' => [],
                    ];
                    break;
                case 'hashtags':
                    return [
                        'accounts' => [],
                        'hashtags' => $this->hashtags(),
                        'statuses' => [],
                    ];
                    break;
                case 'statuses':
                    return [
                        'accounts' => [],
                        'hashtags' => [],
                        'statuses' => $this->statuses(),
                    ];
                    break;
            }
        }

        if ($query->has('account_id')) {
            return [
                'accounts' => [],
                'hashtags' => [],
                'statuses' => $this->statusesById(),
            ];
        }

        return [
            'accounts' => $this->accounts(),
            'hashtags' => $this->hashtags(),
            'statuses' => $this->statuses(),
        ];
    }

    protected function accounts($initalQuery = false)
    {
        $mastodonMode = self::$mastodonMode;
        $user = request()->user();
        $limit = $this->query->input('limit') ?? 20;
        $offset = $this->query->input('offset') ?? 0;
        $rawQuery = $initalQuery ? $initalQuery : $this->query->input('q');
        $query = $rawQuery.'%';
        $webfingerQuery = $query;
        if (Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') {
            $query = '@'.$query;
        }
        if (substr($webfingerQuery, 0, 1) !== '@') {
            $webfingerQuery = '@'.$webfingerQuery;
        }
        $banned = InstanceService::getBannedDomains() ?? [];
        $domainBlocks = UserFilterService::domainBlocks($user->profile_id);
        if ($domainBlocks && count($domainBlocks)) {
            $banned = array_unique(
                array_values(
                    array_merge($banned, $domainBlocks)
                )
            );
        }
        $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
        $results = Profile::select('username', 'id', 'followers_count', 'domain')
            ->where('username', $operator, $query)
            ->orWhere('webfinger', $operator, $webfingerQuery)
            ->orderByDesc('profiles.followers_count')
            ->offset($offset)
            ->limit($limit)
            ->get()
            ->filter(function ($profile) use ($banned) {
                return in_array($profile->domain, $banned) == false;
            })
            ->map(function ($res) use ($mastodonMode) {
                return $mastodonMode ?
                AccountService::getMastodon($res['id']) :
                AccountService::get($res['id']);
            })
            ->filter(function ($account) {
                return $account && isset($account['id']);
            })
            ->values();

        return $results;
    }

    protected function hashtags()
    {
        $mastodonMode = self::$mastodonMode;
        $q = $this->query->input('q');
        $limit = $this->query->input('limit') ?? 20;
        $offset = $this->query->input('offset') ?? 0;
        $query = Str::startsWith($q, '#') ? substr($q, 1).'%' : $q;
        $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';

        return Hashtag::where('name', $operator, $query)
            ->orderByDesc('cached_count')
            ->offset($offset)
            ->limit($limit)
            ->get()
            ->filter(function ($tag) {
                return $tag->can_search != false;
            })
            ->map(function ($tag) use ($mastodonMode) {
                $res = [
                    'name' => $tag->name,
                    'url' => $tag->url(),
                ];

                if (! $mastodonMode) {
                    $res['history'] = [];
                    $res['count'] = $tag->cached_count ?? 0;
                }

                return $res;
            })
            ->values();
    }

    protected function statuses()
    {
        // Removed until we provide more relevent sorting/results
        return [];
    }

    protected function statusesById()
    {
        // Removed until we provide more relevent sorting/results
        return [];
    }

    protected function resolveQuery()
    {
        $default = [
            'accounts' => [],
            'hashtags' => [],
            'statuses' => [],
        ];
        $user = request()->user();
        $mastodonMode = self::$mastodonMode;
        $query = urldecode($this->query->input('q'));
        $banned = InstanceService::getBannedDomains();
        $domainBlocks = UserFilterService::domainBlocks($user->profile_id);
        if ($domainBlocks && count($domainBlocks)) {
            $banned = array_unique(
                array_values(
                    array_merge($banned, $domainBlocks)
                )
            );
        }
        if (substr($query, 0, 1) === '@' && ! Str::contains($query, '.')) {
            $default['accounts'] = $this->accounts(substr($query, 1));

            return $default;
        }
        if (Helpers::validateLocalUrl($query)) {
            if (Str::contains($query, '/p/') || Str::contains($query, 'i/web/post/')) {
                return $this->resolveLocalStatus();
            } elseif (Str::contains($query, 'i/web/profile/')) {
                return $this->resolveLocalProfileId();
            } else {
                return $this->resolveLocalProfile();
            }
        } else {
            if (! Helpers::validateUrl($query) && strpos($query, '@') == -1) {
                return $default;
            }

            if (! Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) {
                try {
                    $res = WebfingerService::lookup('@'.$query, $mastodonMode);
                } catch (\Exception $e) {
                    return $default;
                }
                if ($res && isset($res['id'], $res['url'])) {
                    $domain = strtolower(parse_url($res['url'], PHP_URL_HOST));
                    if (in_array($domain, $banned)) {
                        return $default;
                    }
                    $default['accounts'][] = $res;

                    return $default;
                } else {
                    return $default;
                }
            }

            if (Str::substrCount($query, '@') == 2) {
                try {
                    $res = WebfingerService::lookup($query, $mastodonMode);
                } catch (\Exception $e) {
                    return $default;
                }
                if ($res && isset($res['id'])) {
                    $domain = strtolower(parse_url($res['url'], PHP_URL_HOST));
                    if (in_array($domain, $banned)) {
                        return $default;
                    }
                    $default['accounts'][] = $res;

                    return $default;
                } else {
                    return $default;
                }
            }

            if ($sid = Status::whereUri($query)->first()) {
                $s = StatusService::get($sid->id, false);
                if (! $s) {
                    return $default;
                }
                if (in_array($s['visibility'], ['public', 'unlisted'])) {
                    $default['statuses'][] = $s;

                    return $default;
                }
            }

            try {
                $res = ActivityPubFetchService::get($query);

                if ($res) {
                    $json = json_decode($res, true);

                    if (! $json || ! isset($json['@context']) || ! isset($json['type']) || ! in_array($json['type'], ['Note', 'Person'])) {
                        return [
                            'accounts' => [],
                            'hashtags' => [],
                            'statuses' => [],
                        ];
                    }

                    switch ($json['type']) {
                        case 'Note':
                            $obj = Helpers::statusFetch($query);
                            if (! $obj || ! isset($obj['id'])) {
                                return $default;
                            }
                            $note = $mastodonMode ?
                                StatusService::getMastodon($obj['id'], false) :
                                StatusService::get($obj['id'], false);
                            if (! $note) {
                                return $default;
                            }
                            if (! isset($note['visibility']) || ! in_array($note['visibility'], ['public', 'unlisted'])) {
                                return $default;
                            }
                            $default['statuses'][] = $note;

                            return $default;
                            break;

                        case 'Person':
                            $obj = Helpers::profileFetch($query);
                            if (! $obj) {
                                return $default;
                            }
                            if (in_array($obj['domain'], $banned)) {
                                return $default;
                            }
                            $default['accounts'][] = $mastodonMode ?
                                AccountService::getMastodon($obj['id'], true) :
                                AccountService::get($obj['id'], true);

                            return $default;
                            break;

                        default:
                            return [
                                'accounts' => [],
                                'hashtags' => [],
                                'statuses' => [],
                            ];
                            break;
                    }
                }
            } catch (\Exception $e) {
                return [
                    'accounts' => [],
                    'hashtags' => [],
                    'statuses' => [],
                ];
            }

            return $default;
        }
    }

    protected function resolveLocalStatus()
    {
        $query = urldecode($this->query->input('q'));
        $query = last(explode('/', parse_url($query, PHP_URL_PATH)));
        $status = StatusService::getMastodon($query, false);
        if (! $status || ! in_array($status['visibility'], ['public', 'unlisted'])) {
            return [
                'accounts' => [],
                'hashtags' => [],
                'statuses' => [],
            ];
        }

        $res = [
            'accounts' => [],
            'hashtags' => [],
            'statuses' => [$status],
        ];

        return $res;
    }

    protected function resolveLocalProfile()
    {
        $query = urldecode($this->query->input('q'));
        $query = last(explode('/', parse_url($query, PHP_URL_PATH)));
        $profile = Profile::whereNull('status')
            ->whereNull('domain')
            ->whereUsername($query)
            ->first();

        if (! $profile) {
            return [
                'accounts' => [],
                'hashtags' => [],
                'statuses' => [],
            ];
        }

        $fractal = new Fractal\Manager();
        $fractal->setSerializer(new ArraySerializer());
        $resource = new Fractal\Resource\Item($profile, new AccountTransformer());

        return [
            'accounts' => [$fractal->createData($resource)->toArray()],
            'hashtags' => [],
            'statuses' => [],
        ];
    }

    protected function resolveLocalProfileId()
    {
        $query = urldecode($this->query->input('q'));
        $query = last(explode('/', parse_url($query, PHP_URL_PATH)));
        $profile = Profile::whereNull('status')
            ->find($query);

        if (! $profile) {
            return [
                'accounts' => [],
                'hashtags' => [],
                'statuses' => [],
            ];
        }

        $fractal = new Fractal\Manager();
        $fractal->setSerializer(new ArraySerializer());
        $resource = new Fractal\Resource\Item($profile, new AccountTransformer());

        return [
            'accounts' => [$fractal->createData($resource)->toArray()],
            'hashtags' => [],
            'statuses' => [],
        ];
    }
}