Merge remote-tracking branch 'remotes/origin/develop' into restricted-relations-embedding
[akkoma] / lib / pleroma / web / mastodon_api / controllers / search_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.SearchController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Activity
9 alias Pleroma.Plugs.OAuthScopesPlug
10 alias Pleroma.Plugs.RateLimiter
11 alias Pleroma.Repo
12 alias Pleroma.User
13 alias Pleroma.Web
14 alias Pleroma.Web.ControllerHelper
15 alias Pleroma.Web.MastodonAPI.AccountView
16 alias Pleroma.Web.MastodonAPI.StatusView
17
18 require Logger
19
20 plug(Pleroma.Web.ApiSpec.CastAndValidate)
21
22 # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
23 plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
24
25 # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
26
27 plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
28
29 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
30
31 def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
32 accounts = User.search(query, search_options(params, user))
33
34 conn
35 |> put_view(AccountView)
36 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
37 |> render("index.json",
38 users: accounts,
39 for: user,
40 as: :user,
41 embed_relationships: ControllerHelper.embed_relationships?(params)
42 )
43 end
44
45 def search2(conn, params), do: do_search(:v2, conn, params)
46 def search(conn, params), do: do_search(:v1, conn, params)
47
48 defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
49 options = search_options(params, user)
50 timeout = Keyword.get(Repo.config(), :timeout, 15_000)
51 default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
52
53 result =
54 default_values
55 |> Enum.map(fn {resource, default_value} ->
56 if params[:type] in [nil, resource] do
57 {resource, fn -> resource_search(version, resource, query, options) end}
58 else
59 {resource, fn -> default_value end}
60 end
61 end)
62 |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
63 timeout: timeout,
64 on_timeout: :kill_task
65 )
66 |> Enum.reduce(default_values, fn
67 {:ok, {resource, result}}, acc ->
68 Map.put(acc, resource, result)
69
70 _error, acc ->
71 acc
72 end)
73
74 json(conn, result)
75 end
76
77 defp search_options(params, user) do
78 [
79 resolve: params[:resolve],
80 following: params[:following],
81 limit: params[:limit],
82 offset: params[:offset],
83 type: params[:type],
84 author: get_author(params),
85 embed_relationships: ControllerHelper.embed_relationships?(params),
86 for_user: user
87 ]
88 |> Enum.filter(&elem(&1, 1))
89 end
90
91 defp resource_search(_, "accounts", query, options) do
92 accounts = with_fallback(fn -> User.search(query, options) end)
93
94 AccountView.render("index.json",
95 users: accounts,
96 for: options[:for_user],
97 as: :user,
98 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
99 embed_relationships: options[:embed_relationships]
100 )
101 end
102
103 defp resource_search(_, "statuses", query, options) do
104 statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
105
106 StatusView.render("index.json",
107 activities: statuses,
108 for: options[:for_user],
109 as: :activity
110 )
111 end
112
113 defp resource_search(:v2, "hashtags", query, _options) do
114 tags_path = Web.base_url() <> "/tag/"
115
116 query
117 |> prepare_tags()
118 |> Enum.map(fn tag ->
119 tag = String.trim_leading(tag, "#")
120 %{name: tag, url: tags_path <> tag}
121 end)
122 end
123
124 defp resource_search(:v1, "hashtags", query, _options) do
125 query
126 |> prepare_tags()
127 |> Enum.map(fn tag -> String.trim_leading(tag, "#") end)
128 end
129
130 defp prepare_tags(query) do
131 query
132 |> String.split()
133 |> Enum.uniq()
134 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
135 end
136
137 defp with_fallback(f, fallback \\ []) do
138 try do
139 f.()
140 rescue
141 error ->
142 Logger.error("#{__MODULE__} search error: #{inspect(error)}")
143 fallback
144 end
145 end
146
147 defp get_author(%{account_id: account_id}) when is_binary(account_id),
148 do: User.get_cached_by_id(account_id)
149
150 defp get_author(_params), do: nil
151 end