diff options
author | 2023-06-21 18:26:40 +0200 | |
---|---|---|
committer | 2023-06-21 17:26:40 +0100 | |
commit | 831ae09f8bab04af854243421047371339c3e190 (patch) | |
tree | f7709d478cc363dc1899bdb658fe20e2dc7986f3 /internal/processing/search.go | |
parent | [docs] Disambiguate docker version, don't recommend opening localhost (#1913) (diff) | |
download | gotosocial-831ae09f8bab04af854243421047371339c3e190.tar.xz |
[feature] Add partial text search for accounts + statuses (#1836)
Diffstat (limited to 'internal/processing/search.go')
-rw-r--r-- | internal/processing/search.go | 295 |
1 files changed, 0 insertions, 295 deletions
diff --git a/internal/processing/search.go b/internal/processing/search.go deleted file mode 100644 index ef5da9ee7..000000000 --- a/internal/processing/search.go +++ /dev/null @@ -1,295 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// 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 <http://www.gnu.org/licenses/>. - -package processing - -import ( - "context" - "errors" - "fmt" - "net/url" - "strings" - - "codeberg.org/gruf/go-kv" - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" - "github.com/superseriousbusiness/gotosocial/internal/gtscontext" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -// Implementation note: in this function, we tend to log errors -// at debug level rather than return them. This is because the -// search has a sort of fallthrough logic: if we can't get a result -// with x search, we should try with y search rather than returning. -// -// If we get to the end and still haven't found anything, even then -// we shouldn't return an error, just return an empty search result. -// -// The only exception to this is when we get a malformed query, in -// which case we return a bad request error so the user knows they -// did something funky. -func (p *Processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) { - // tidy up the query and make sure it wasn't just spaces - query := strings.TrimSpace(search.Query) - if query == "" { - err := errors.New("search query was empty string after trimming space") - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - - l := log.WithContext(ctx). - WithFields(kv.Fields{{"query", query}}...) - - searchResult := &apimodel.SearchResult{ - Accounts: []apimodel.Account{}, - Statuses: []apimodel.Status{}, - Hashtags: []apimodel.Tag{}, - } - - // currently the search will only ever return one result, - // so return nothing if the offset is greater than 0 - if search.Offset > 0 { - return searchResult, nil - } - - foundAccounts := []*gtsmodel.Account{} - foundStatuses := []*gtsmodel.Status{} - - var foundOne bool - - /* - SEARCH BY MENTION - check if the query is something like @whatever_username@example.org -- this means it's likely a remote account - */ - maybeNamestring := query - if maybeNamestring[0] != '@' { - maybeNamestring = "@" + maybeNamestring - } - - if username, domain, err := util.ExtractNamestringParts(maybeNamestring); err == nil { - l.Trace("search term is a mention, looking it up...") - blocked, err := p.state.DB.IsDomainBlocked(ctx, domain) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking domain block: %w", err)) - } - if blocked { - l.Debug("domain is blocked") - return searchResult, nil - } - - foundAccount, err := p.searchAccountByUsernameDomain(ctx, authed, username, domain, search.Resolve) - if err != nil { - var errNotRetrievable *dereferencing.ErrNotRetrievable - if !errors.As(err, &errNotRetrievable) { - // return a proper error only if it wasn't just not retrievable - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error looking up account: %w", err)) - } - return searchResult, nil - } - - foundAccounts = append(foundAccounts, foundAccount) - foundOne = true - l.Trace("got an account by searching by mention") - } - - /* - SEARCH BY URI - check if the query is a URI with a recognizable scheme and dereference it - */ - if !foundOne { - if uri, err := url.Parse(query); err == nil { - if uri.Scheme == "https" || uri.Scheme == "http" { - l.Trace("search term is a uri, looking it up...") - blocked, err := p.state.DB.IsURIBlocked(ctx, uri) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking domain block: %w", err)) - } - if blocked { - l.Debug("domain is blocked") - return searchResult, nil - } - - // check if it's a status... - foundStatus, err := p.searchStatusByURI(ctx, authed, uri) - if err != nil { - // Check for semi-expected error types. - var ( - errNotRetrievable *dereferencing.ErrNotRetrievable - errWrongType *ap.ErrWrongType - ) - if !errors.As(err, &errNotRetrievable) && !errors.As(err, &errWrongType) { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error looking up status: %w", err)) - } - } else { - foundStatuses = append(foundStatuses, foundStatus) - foundOne = true - l.Trace("got a status by searching by URI") - } - - // ... or an account - if !foundOne { - foundAccount, err := p.searchAccountByURI(ctx, authed, uri, search.Resolve) - if err != nil { - // Check for semi-expected error types. - var ( - errNotRetrievable *dereferencing.ErrNotRetrievable - errWrongType *ap.ErrWrongType - ) - if !errors.As(err, &errNotRetrievable) && !errors.As(err, &errWrongType) { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error looking up account: %w", err)) - } - } else { - foundAccounts = append(foundAccounts, foundAccount) - foundOne = true - l.Trace("got an account by searching by URI") - } - } - } - } - } - - if !foundOne { - // we got nothing, we can return early - l.Trace("found nothing, returning") - return searchResult, nil - } - - /* - FROM HERE ON we have our search results, it's just a matter of filtering them according to what this user is allowed to see, - and then converting them into our frontend format. - */ - for _, foundAccount := range foundAccounts { - // make sure there's no block in either direction between the account and the requester - blocked, err := p.state.DB.IsEitherBlocked(ctx, authed.Account.ID, foundAccount.ID) - if err != nil { - err = fmt.Errorf("SearchGet: error checking block between %s and %s: %s", authed.Account.ID, foundAccount.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - l.Tracef("block exists between %s and %s, skipping this result", authed.Account.ID, foundAccount.ID) - continue - } - - apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, foundAccount) - if err != nil { - err = fmt.Errorf("SearchGet: error converting account %s to api account: %s", foundAccount.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - searchResult.Accounts = append(searchResult.Accounts, *apiAcct) - } - - for _, foundStatus := range foundStatuses { - // make sure each found status is visible to the requester - visible, err := p.filter.StatusVisible(ctx, authed.Account, foundStatus) - if err != nil { - err = fmt.Errorf("SearchGet: error checking visibility of status %s for account %s: %s", foundStatus.ID, authed.Account.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - if !visible { - l.Tracef("status %s is not visible to account %s, skipping this result", foundStatus.ID, authed.Account.ID) - continue - } - - apiStatus, err := p.tc.StatusToAPIStatus(ctx, foundStatus, authed.Account) - if err != nil { - err = fmt.Errorf("SearchGet: error converting status %s to api status: %s", foundStatus.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - searchResult.Statuses = append(searchResult.Statuses, *apiStatus) - } - - return searchResult, nil -} - -func (p *Processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL) (*gtsmodel.Status, error) { - status, _, err := p.federator.GetStatusByURI(gtscontext.SetFastFail(ctx), authed.Account.Username, uri) - return status, err -} - -func (p *Processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) { - if !resolve { - var ( - account *gtsmodel.Account - err error - uriStr = uri.String() - ) - - // Search the database for existing account with ID URI. - account, err = p.state.DB.GetAccountByURI(ctx, uriStr) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return nil, fmt.Errorf("searchAccountByURI: error checking database for account %s: %w", uriStr, err) - } - - if account == nil { - // Else, search the database for existing by ID URL. - account, err = p.state.DB.GetAccountByURL(ctx, uriStr) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - return nil, fmt.Errorf("searchAccountByURI: error checking database for account %s: %w", uriStr, err) - } - return nil, dereferencing.NewErrNotRetrievable(err) - } - } - - return account, nil - } - - account, _, err := p.federator.GetAccountByURI( - gtscontext.SetFastFail(ctx), - authed.Account.Username, - uri, - ) - return account, err -} - -func (p *Processor) searchAccountByUsernameDomain(ctx context.Context, authed *oauth.Auth, username string, domain string, resolve bool) (*gtsmodel.Account, error) { - if !resolve { - if domain == config.GetHost() || domain == config.GetAccountDomain() { - // We do local lookups using an empty domain, - // else it will fail the db search below. - domain = "" - } - - // Search the database for existing account with USERNAME@DOMAIN - account, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, domain) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - return nil, fmt.Errorf("searchAccountByUsernameDomain: error checking database for account %s@%s: %w", username, domain, err) - } - return nil, dereferencing.NewErrNotRetrievable(err) - } - - return account, nil - } - - account, _, err := p.federator.GetAccountByUsernameDomain( - gtscontext.SetFastFail(ctx), - authed.Account.Username, - username, domain, - ) - return account, err -} |