mirror of
				https://github.com/superseriousbusiness/gotosocial
				synced 2025-06-05 21:59:39 +02:00 
			
		
		
		
	[feature] Add from: search operator and account_id query param (#2943)
* Add from: search operator * Fix whitespace in Swagger YAML comment * Move query parsing into its own method * Document search * Clarify post search scope
This commit is contained in:
		@@ -62,14 +62,15 @@ func (p *Processor) Get(
 | 
			
		||||
	req *apimodel.SearchRequest,
 | 
			
		||||
) (*apimodel.SearchResult, gtserror.WithCode) {
 | 
			
		||||
	var (
 | 
			
		||||
		maxID     = req.MaxID
 | 
			
		||||
		minID     = req.MinID
 | 
			
		||||
		limit     = req.Limit
 | 
			
		||||
		offset    = req.Offset
 | 
			
		||||
		query     = strings.TrimSpace(req.Query)                      // Trim trailing/leading whitespace.
 | 
			
		||||
		queryType = strings.TrimSpace(strings.ToLower(req.QueryType)) // Trim trailing/leading whitespace; convert to lowercase.
 | 
			
		||||
		resolve   = req.Resolve
 | 
			
		||||
		following = req.Following
 | 
			
		||||
		maxID         = req.MaxID
 | 
			
		||||
		minID         = req.MinID
 | 
			
		||||
		limit         = req.Limit
 | 
			
		||||
		offset        = req.Offset
 | 
			
		||||
		query         = strings.TrimSpace(req.Query)                      // Trim trailing/leading whitespace.
 | 
			
		||||
		queryType     = strings.TrimSpace(strings.ToLower(req.QueryType)) // Trim trailing/leading whitespace; convert to lowercase.
 | 
			
		||||
		resolve       = req.Resolve
 | 
			
		||||
		following     = req.Following
 | 
			
		||||
		fromAccountID = req.AccountID
 | 
			
		||||
 | 
			
		||||
		// Include instance accounts in the first
 | 
			
		||||
		// parts of this search. This will be
 | 
			
		||||
@@ -114,6 +115,7 @@ func (p *Processor) Get(
 | 
			
		||||
			{"queryType", queryType},
 | 
			
		||||
			{"resolve", resolve},
 | 
			
		||||
			{"following", following},
 | 
			
		||||
			{"fromAccountID", fromAccountID},
 | 
			
		||||
		}...).
 | 
			
		||||
		Debugf("beginning search")
 | 
			
		||||
 | 
			
		||||
@@ -309,6 +311,7 @@ func (p *Processor) Get(
 | 
			
		||||
		query,
 | 
			
		||||
		queryType,
 | 
			
		||||
		following,
 | 
			
		||||
		fromAccountID,
 | 
			
		||||
		appendAccount,
 | 
			
		||||
		appendStatus,
 | 
			
		||||
	); err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
			
		||||
@@ -743,6 +746,7 @@ func (p *Processor) byText(
 | 
			
		||||
	query string,
 | 
			
		||||
	queryType string,
 | 
			
		||||
	following bool,
 | 
			
		||||
	fromAccountID string,
 | 
			
		||||
	appendAccount func(*gtsmodel.Account),
 | 
			
		||||
	appendStatus func(*gtsmodel.Status),
 | 
			
		||||
) error {
 | 
			
		||||
@@ -779,6 +783,7 @@ func (p *Processor) byText(
 | 
			
		||||
			limit,
 | 
			
		||||
			offset,
 | 
			
		||||
			query,
 | 
			
		||||
			fromAccountID,
 | 
			
		||||
			appendStatus,
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
@@ -826,12 +831,30 @@ func (p *Processor) statusesByText(
 | 
			
		||||
	limit int,
 | 
			
		||||
	offset int,
 | 
			
		||||
	query string,
 | 
			
		||||
	fromAccountID string,
 | 
			
		||||
	appendStatus func(*gtsmodel.Status),
 | 
			
		||||
) error {
 | 
			
		||||
	parsed, err := p.parseQuery(ctx, query)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	query = parsed.query
 | 
			
		||||
	// If the owning account for statuses was not provided as the account_id query parameter,
 | 
			
		||||
	// it may still have been provided as a search operator in the query string.
 | 
			
		||||
	if fromAccountID == "" {
 | 
			
		||||
		fromAccountID = parsed.fromAccountID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	statuses, err := p.state.DB.SearchForStatuses(
 | 
			
		||||
		ctx,
 | 
			
		||||
		requestingAccountID,
 | 
			
		||||
		query, maxID, minID, limit, offset)
 | 
			
		||||
		query,
 | 
			
		||||
		fromAccountID,
 | 
			
		||||
		maxID,
 | 
			
		||||
		minID,
 | 
			
		||||
		limit,
 | 
			
		||||
		offset,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
			
		||||
		return gtserror.Newf("error checking database for statuses using text %s: %w", query, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -842,3 +865,60 @@ func (p *Processor) statusesByText(
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// parsedQuery represents the results of parsing the search operator terms within a query.
 | 
			
		||||
type parsedQuery struct {
 | 
			
		||||
	// query is the original search query text with operator terms removed.
 | 
			
		||||
	query string
 | 
			
		||||
	// fromAccountID is the account from a successfully resolved `from:` operator, if present.
 | 
			
		||||
	fromAccountID string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// parseQuery parses query text and handles any search operator terms present.
 | 
			
		||||
func (p *Processor) parseQuery(ctx context.Context, query string) (parsed parsedQuery, err error) {
 | 
			
		||||
	queryPartSeparator := " "
 | 
			
		||||
	queryParts := strings.Split(query, queryPartSeparator)
 | 
			
		||||
	nonOperatorQueryParts := make([]string, 0, len(queryParts))
 | 
			
		||||
	for _, queryPart := range queryParts {
 | 
			
		||||
		if arg, hasPrefix := strings.CutPrefix(queryPart, "from:"); hasPrefix {
 | 
			
		||||
			parsed.fromAccountID, err = p.parseFromOperatorArg(ctx, arg)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			nonOperatorQueryParts = append(nonOperatorQueryParts, queryPart)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	parsed.query = strings.Join(nonOperatorQueryParts, queryPartSeparator)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// parseFromOperatorArg attempts to parse the from: operator's argument as an account name,
 | 
			
		||||
// and returns the account ID if possible. Allows specifying an account name with or without a leading @.
 | 
			
		||||
func (p *Processor) parseFromOperatorArg(ctx context.Context, namestring string) (string, error) {
 | 
			
		||||
	if namestring == "" {
 | 
			
		||||
		return "", gtserror.New(
 | 
			
		||||
			"the 'from:' search operator requires an account name, but it wasn't provided",
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
	if namestring[0] != '@' {
 | 
			
		||||
		namestring = "@" + namestring
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	username, domain, err := util.ExtractNamestringParts(namestring)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", gtserror.Newf(
 | 
			
		||||
			"the 'from:' search operator couldn't parse its argument as an account name: %w",
 | 
			
		||||
			err,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
	account, err := p.state.DB.GetAccountByUsernameDomain(gtscontext.SetBarebones(ctx), username, domain)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", gtserror.Newf(
 | 
			
		||||
			"the 'from:' search operator couldn't find the requested account name: %w",
 | 
			
		||||
			err,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return account.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user