Merge branch 'preload-data' of git.pleroma.social:stwf/pleroma into preload-data
[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 |> render("index.json",
37 users: accounts,
38 for: user,
39 as: :user
40 )
41 end
42
43 def search2(conn, params), do: do_search(:v2, conn, params)
44 def search(conn, params), do: do_search(:v1, conn, params)
45
46 defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
47 options = search_options(params, user)
48 timeout = Keyword.get(Repo.config(), :timeout, 15_000)
49 default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
50
51 result =
52 default_values
53 |> Enum.map(fn {resource, default_value} ->
54 if params[:type] in [nil, resource] do
55 {resource, fn -> resource_search(version, resource, query, options) end}
56 else
57 {resource, fn -> default_value end}
58 end
59 end)
60 |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
61 timeout: timeout,
62 on_timeout: :kill_task
63 )
64 |> Enum.reduce(default_values, fn
65 {:ok, {resource, result}}, acc ->
66 Map.put(acc, resource, result)
67
68 _error, acc ->
69 acc
70 end)
71
72 json(conn, result)
73 end
74
75 defp search_options(params, user) do
76 [
77 resolve: params[:resolve],
78 following: params[:following],
79 limit: params[:limit],
80 offset: params[:offset],
81 type: params[:type],
82 author: get_author(params),
83 embed_relationships: ControllerHelper.embed_relationships?(params),
84 for_user: user
85 ]
86 |> Enum.filter(&elem(&1, 1))
87 end
88
89 defp resource_search(_, "accounts", query, options) do
90 accounts = with_fallback(fn -> User.search(query, options) end)
91
92 AccountView.render("index.json",
93 users: accounts,
94 for: options[:for_user],
95 as: :user,
96 embed_relationships: options[:embed_relationships]
97 )
98 end
99
100 defp resource_search(_, "statuses", query, options) do
101 statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
102
103 StatusView.render("index.json",
104 activities: statuses,
105 for: options[:for_user],
106 as: :activity
107 )
108 end
109
110 defp resource_search(:v2, "hashtags", query, _options) do
111 tags_path = Web.base_url() <> "/tag/"
112
113 query
114 |> prepare_tags()
115 |> Enum.map(fn tag ->
116 %{name: tag, url: tags_path <> tag}
117 end)
118 end
119
120 defp resource_search(:v1, "hashtags", query, _options) do
121 prepare_tags(query)
122 end
123
124 defp prepare_tags(query, add_joined_tag \\ true) do
125 tags =
126 query
127 |> String.split(~r/[^#\w]+/u, trim: true)
128 |> Enum.uniq_by(&String.downcase/1)
129
130 explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
131
132 tags =
133 if Enum.any?(explicit_tags) do
134 explicit_tags
135 else
136 tags
137 end
138
139 tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
140
141 if Enum.empty?(explicit_tags) && add_joined_tag do
142 tags
143 |> Kernel.++([joined_tag(tags)])
144 |> Enum.uniq_by(&String.downcase/1)
145 else
146 tags
147 end
148 end
149
150 defp joined_tag(tags) do
151 tags
152 |> Enum.map(fn tag -> String.capitalize(tag) end)
153 |> Enum.join()
154 end
155
156 defp with_fallback(f, fallback \\ []) do
157 try do
158 f.()
159 rescue
160 error ->
161 Logger.error("#{__MODULE__} search error: #{inspect(error)}")
162 fallback
163 end
164 end
165
166 defp get_author(%{account_id: account_id}) when is_binary(account_id),
167 do: User.get_cached_by_id(account_id)
168
169 defp get_author(_params), do: nil
170 end