Merge remote-tracking branch 'remotes/origin/develop' into oauth-scopes-tweaks-and...
[akkoma] / lib / pleroma / web / twitter_api / controllers / util_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.TwitterAPI.UtilController do
6 use Pleroma.Web, :controller
7
8 require Logger
9
10 alias Pleroma.Activity
11 alias Pleroma.Config
12 alias Pleroma.Emoji
13 alias Pleroma.Healthcheck
14 alias Pleroma.Notification
15 alias Pleroma.Plugs.AuthenticationPlug
16 alias Pleroma.Plugs.OAuthScopesPlug
17 alias Pleroma.User
18 alias Pleroma.Web
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.WebFinger
21
22 plug(
23 OAuthScopesPlug,
24 %{scopes: ["follow", "write:follows"]}
25 when action == :follow_import
26 )
27
28 # Note: follower can submit the form (with password auth) not being signed in (having no token)
29 plug(
30 OAuthScopesPlug,
31 %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
32 when action == :do_remote_follow
33 )
34
35 plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
36
37 plug(
38 OAuthScopesPlug,
39 %{scopes: ["write:accounts"]}
40 when action in [
41 :change_email,
42 :change_password,
43 :delete_account,
44 :update_notificaton_settings,
45 :disable_account
46 ]
47 )
48
49 plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
50
51 plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
52
53 def help_test(conn, _params) do
54 json(conn, "ok")
55 end
56
57 def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
58 with %User{} = user <- User.get_cached_by_nickname(nick),
59 avatar = User.avatar_url(user) do
60 conn
61 |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
62 else
63 _e ->
64 render(conn, "subscribe.html", %{
65 nickname: nick,
66 avatar: nil,
67 error: "Could not find user"
68 })
69 end
70 end
71
72 def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
73 with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
74 %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
75 conn
76 |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
77 else
78 _e ->
79 render(conn, "subscribe.html", %{
80 nickname: nick,
81 avatar: nil,
82 error: "Something went wrong."
83 })
84 end
85 end
86
87 def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
88 if is_status?(acct) do
89 {:ok, object} = Pleroma.Object.Fetcher.fetch_object_from_id(acct)
90 %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"])
91 redirect(conn, to: "/notice/#{activity_id}")
92 else
93 with {:ok, followee} <- User.get_or_fetch(acct) do
94 conn
95 |> render(follow_template(user), %{
96 error: false,
97 acct: acct,
98 avatar: User.avatar_url(followee),
99 name: followee.nickname,
100 id: followee.id
101 })
102 else
103 {:error, _reason} ->
104 render(conn, follow_template(user), %{error: :error})
105 end
106 end
107 end
108
109 defp follow_template(%User{} = _user), do: "follow.html"
110 defp follow_template(_), do: "follow_login.html"
111
112 defp is_status?(acct) do
113 case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do
114 {:ok, %{"type" => type}}
115 when type in ["Article", "Event", "Note", "Video", "Page", "Question"] ->
116 true
117
118 _ ->
119 false
120 end
121 end
122
123 def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}})
124 when not is_nil(user) do
125 with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
126 {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do
127 conn
128 |> render("followed.html", %{error: false})
129 else
130 # Was already following user
131 {:error, "Could not follow user:" <> _rest} ->
132 render(conn, "followed.html", %{error: "Error following account"})
133
134 {:fetch_user, error} ->
135 Logger.debug("Remote follow failed with error #{inspect(error)}")
136 render(conn, "followed.html", %{error: "Could not find user"})
137
138 e ->
139 Logger.debug("Remote follow failed with error #{inspect(e)}")
140 render(conn, "followed.html", %{error: "Something went wrong."})
141 end
142 end
143
144 # Note: "id" is the id of followee user, disregard incorrect placing under "authorization"
145 def do_remote_follow(conn, %{
146 "authorization" => %{"name" => username, "password" => password, "id" => id}
147 }) do
148 with %User{} = followee <- User.get_cached_by_id(id),
149 {_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee},
150 {_, true, _} <- {
151 :auth,
152 AuthenticationPlug.checkpw(password, user.password_hash),
153 followee
154 },
155 {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do
156 conn
157 |> render("followed.html", %{error: false})
158 else
159 # Was already following user
160 {:error, "Could not follow user:" <> _rest} ->
161 render(conn, "followed.html", %{error: "Error following account"})
162
163 {:auth, _, followee} ->
164 conn
165 |> render("follow_login.html", %{
166 error: "Wrong username or password",
167 id: id,
168 name: followee.nickname,
169 avatar: User.avatar_url(followee)
170 })
171
172 e ->
173 Logger.debug("Remote follow failed with error #{inspect(e)}")
174 render(conn, "followed.html", %{error: "Something went wrong."})
175 end
176 end
177
178 def do_remote_follow(%{assigns: %{user: nil}} = conn, _) do
179 render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
180 end
181
182 def do_remote_follow(conn, _) do
183 render(conn, "followed.html", %{error: "Something went wrong."})
184 end
185
186 def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
187 with {:ok, _} <- Notification.read_one(user, notification_id) do
188 json(conn, %{status: "success"})
189 else
190 {:error, message} ->
191 conn
192 |> put_resp_content_type("application/json")
193 |> send_resp(403, Jason.encode!(%{"error" => message}))
194 end
195 end
196
197 def config(%{assigns: %{format: "xml"}} = conn, _params) do
198 instance = Pleroma.Config.get(:instance)
199
200 response = """
201 <config>
202 <site>
203 <name>#{Keyword.get(instance, :name)}</name>
204 <site>#{Web.base_url()}</site>
205 <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
206 <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
207 </site>
208 </config>
209 """
210
211 conn
212 |> put_resp_content_type("application/xml")
213 |> send_resp(200, response)
214 end
215
216 def config(conn, _params) do
217 instance = Pleroma.Config.get(:instance)
218
219 vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
220
221 uploadlimit = %{
222 uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
223 avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
224 backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
225 bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
226 }
227
228 data = %{
229 name: Keyword.get(instance, :name),
230 description: Keyword.get(instance, :description),
231 server: Web.base_url(),
232 textlimit: to_string(Keyword.get(instance, :limit)),
233 uploadlimit: uploadlimit,
234 closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"),
235 private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"),
236 vapidPublicKey: vapid_public_key,
237 accountActivationRequired:
238 bool_to_val(Keyword.get(instance, :account_activation_required, false)),
239 invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)),
240 safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions]))
241 }
242
243 managed_config = Keyword.get(instance, :managed_config)
244
245 data =
246 if managed_config do
247 pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
248 Map.put(data, "pleromafe", pleroma_fe)
249 else
250 data
251 end
252
253 json(conn, %{site: data})
254 end
255
256 defp bool_to_val(true), do: "1"
257 defp bool_to_val(_), do: "0"
258 defp bool_to_val(true, val, _), do: val
259 defp bool_to_val(_, _, val), do: val
260
261 def frontend_configurations(conn, _params) do
262 config =
263 Pleroma.Config.get(:frontend_configurations, %{})
264 |> Enum.into(%{})
265
266 json(conn, config)
267 end
268
269 def version(%{assigns: %{format: "xml"}} = conn, _params) do
270 version = Pleroma.Application.named_version()
271
272 conn
273 |> put_resp_content_type("application/xml")
274 |> send_resp(200, "<version>#{version}</version>")
275 end
276
277 def version(conn, _params) do
278 json(conn, Pleroma.Application.named_version())
279 end
280
281 def emoji(conn, _params) do
282 emoji =
283 Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
284 Map.put(acc, code, %{image_url: file, tags: tags})
285 end)
286
287 json(conn, emoji)
288 end
289
290 def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
291 with {:ok, _} <- User.update_notification_settings(user, params) do
292 json(conn, %{status: "success"})
293 end
294 end
295
296 def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
297 follow_import(conn, %{"list" => File.read!(listfile.path)})
298 end
299
300 def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
301 with lines <- String.split(list, "\n"),
302 followed_identifiers <-
303 Enum.map(lines, fn line ->
304 String.split(line, ",") |> List.first()
305 end)
306 |> List.delete("Account address") do
307 User.follow_import(follower, followed_identifiers)
308 json(conn, "job started")
309 end
310 end
311
312 def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
313 blocks_import(conn, %{"list" => File.read!(listfile.path)})
314 end
315
316 def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
317 with blocked_identifiers <- String.split(list) do
318 User.blocks_import(blocker, blocked_identifiers)
319 json(conn, "job started")
320 end
321 end
322
323 def change_password(%{assigns: %{user: user}} = conn, params) do
324 case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
325 {:ok, user} ->
326 with {:ok, _user} <-
327 User.reset_password(user, %{
328 password: params["new_password"],
329 password_confirmation: params["new_password_confirmation"]
330 }) do
331 json(conn, %{status: "success"})
332 else
333 {:error, changeset} ->
334 {_, {error, _}} = Enum.at(changeset.errors, 0)
335 json(conn, %{error: "New password #{error}."})
336
337 _ ->
338 json(conn, %{error: "Unable to change password."})
339 end
340
341 {:error, msg} ->
342 json(conn, %{error: msg})
343 end
344 end
345
346 def change_email(%{assigns: %{user: user}} = conn, params) do
347 case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
348 {:ok, user} ->
349 with {:ok, _user} <- User.change_email(user, params["email"]) do
350 json(conn, %{status: "success"})
351 else
352 {:error, changeset} ->
353 {_, {error, _}} = Enum.at(changeset.errors, 0)
354 json(conn, %{error: "Email #{error}."})
355
356 _ ->
357 json(conn, %{error: "Unable to change email."})
358 end
359
360 {:error, msg} ->
361 json(conn, %{error: msg})
362 end
363 end
364
365 def delete_account(%{assigns: %{user: user}} = conn, params) do
366 password = params["password"] || ""
367
368 case CommonAPI.Utils.confirm_current_password(user, password) do
369 {:ok, user} ->
370 User.delete(user)
371 json(conn, %{status: "success"})
372
373 {:error, msg} ->
374 json(conn, %{error: msg})
375 end
376 end
377
378 def disable_account(%{assigns: %{user: user}} = conn, params) do
379 case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
380 {:ok, user} ->
381 User.deactivate_async(user)
382 json(conn, %{status: "success"})
383
384 {:error, msg} ->
385 json(conn, %{error: msg})
386 end
387 end
388
389 def captcha(conn, _params) do
390 json(conn, Pleroma.Captcha.new())
391 end
392
393 def healthcheck(conn, _params) do
394 with true <- Config.get([:instance, :healthcheck]),
395 %{healthy: true} = info <- Healthcheck.system_info() do
396 json(conn, info)
397 else
398 %{healthy: false} = info ->
399 service_unavailable(conn, info)
400
401 _ ->
402 service_unavailable(conn, %{})
403 end
404 end
405
406 defp service_unavailable(conn, info) do
407 conn
408 |> put_status(:service_unavailable)
409 |> json(info)
410 end
411 end