[#2497] Specified SHELL in .gitlab-ci.yml as required for `exexec`.
[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 tag = String.trim_leading(tag, "#")
117 %{name: tag, url: tags_path <> tag}
118 end)
119 end
120
121 defp resource_search(:v1, "hashtags", query, _options) do
122 query
123 |> prepare_tags()
124 |> Enum.map(fn tag -> String.trim_leading(tag, "#") end)
125 end
126
127 defp prepare_tags(query) do
128 query
129 |> String.split()
130 |> Enum.uniq()
131 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
132 end
133
134 defp with_fallback(f, fallback \\ []) do
135 try do
136 f.()
137 rescue
138 error ->
139 Logger.error("#{__MODULE__} search error: #{inspect(error)}")
140 fallback
141 end
142 end
143
144 defp get_author(%{account_id: account_id}) when is_binary(account_id),
145 do: User.get_cached_by_id(account_id)
146
147 defp get_author(_params), do: nil
148 end