OAuthScopesPlug: disallowed nil token (unless with :fallback option). WIP: controller...
[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}} when type in ["Article", "Note", "Video", "Page", "Question"] ->
115 true
116
117 _ ->
118 false
119 end
120 end
121
122 def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}})
123 when not is_nil(user) do
124 with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
125 {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do
126 conn
127 |> render("followed.html", %{error: false})
128 else
129 # Was already following user
130 {:error, "Could not follow user:" <> _rest} ->
131 render(conn, "followed.html", %{error: "Error following account"})
132
133 {:fetch_user, error} ->
134 Logger.debug("Remote follow failed with error #{inspect(error)}")
135 render(conn, "followed.html", %{error: "Could not find user"})
136
137 e ->
138 Logger.debug("Remote follow failed with error #{inspect(e)}")
139 render(conn, "followed.html", %{error: "Something went wrong."})
140 end
141 end
142
143 # Note: "id" is the id of followee user, disregard incorrect placing under "authorization"
144 def do_remote_follow(conn, %{
145 "authorization" => %{"name" => username, "password" => password, "id" => id}
146 }) do
147 with %User{} = followee <- User.get_cached_by_id(id),
148 {_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee},
149 {_, true, _} <- {
150 :auth,
151 AuthenticationPlug.checkpw(password, user.password_hash),
152 followee
153 },
154 {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do
155 conn
156 |> render("followed.html", %{error: false})
157 else
158 # Was already following user
159 {:error, "Could not follow user:" <> _rest} ->
160 render(conn, "followed.html", %{error: "Error following account"})
161
162 {:auth, _, followee} ->
163 conn
164 |> render("follow_login.html", %{
165 error: "Wrong username or password",
166 id: id,
167 name: followee.nickname,
168 avatar: User.avatar_url(followee)
169 })
170
171 e ->
172 Logger.debug("Remote follow failed with error #{inspect(e)}")
173 render(conn, "followed.html", %{error: "Something went wrong."})
174 end
175 end
176
177 def do_remote_follow(%{assigns: %{user: nil}} = conn, _) do
178 render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
179 end
180
181 def do_remote_follow(conn, _) do
182 render(conn, "followed.html", %{error: "Something went wrong."})
183 end
184
185 def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
186 with {:ok, _} <- Notification.read_one(user, notification_id) do
187 json(conn, %{status: "success"})
188 else
189 {:error, message} ->
190 conn
191 |> put_resp_content_type("application/json")
192 |> send_resp(403, Jason.encode!(%{"error" => message}))
193 end
194 end
195
196 def config(%{assigns: %{format: "xml"}} = conn, _params) do
197 instance = Pleroma.Config.get(:instance)
198
199 response = """
200 <config>
201 <site>
202 <name>#{Keyword.get(instance, :name)}</name>
203 <site>#{Web.base_url()}</site>
204 <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
205 <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
206 </site>
207 </config>
208 """
209
210 conn
211 |> put_resp_content_type("application/xml")
212 |> send_resp(200, response)
213 end
214
215 def config(conn, _params) do
216 instance = Pleroma.Config.get(:instance)
217
218 vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
219
220 uploadlimit = %{
221 uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
222 avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
223 backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
224 bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
225 }
226
227 data = %{
228 name: Keyword.get(instance, :name),
229 description: Keyword.get(instance, :description),
230 server: Web.base_url(),
231 textlimit: to_string(Keyword.get(instance, :limit)),
232 uploadlimit: uploadlimit,
233 closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"),
234 private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"),
235 vapidPublicKey: vapid_public_key,
236 accountActivationRequired:
237 bool_to_val(Keyword.get(instance, :account_activation_required, false)),
238 invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)),
239 safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions]))
240 }
241
242 managed_config = Keyword.get(instance, :managed_config)
243
244 data =
245 if managed_config do
246 pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
247 Map.put(data, "pleromafe", pleroma_fe)
248 else
249 data
250 end
251
252 json(conn, %{site: data})
253 end
254
255 defp bool_to_val(true), do: "1"
256 defp bool_to_val(_), do: "0"
257 defp bool_to_val(true, val, _), do: val
258 defp bool_to_val(_, _, val), do: val
259
260 def frontend_configurations(conn, _params) do
261 config =
262 Pleroma.Config.get(:frontend_configurations, %{})
263 |> Enum.into(%{})
264
265 json(conn, config)
266 end
267
268 def version(%{assigns: %{format: "xml"}} = conn, _params) do
269 version = Pleroma.Application.named_version()
270
271 conn
272 |> put_resp_content_type("application/xml")
273 |> send_resp(200, "<version>#{version}</version>")
274 end
275
276 def version(conn, _params) do
277 json(conn, Pleroma.Application.named_version())
278 end
279
280 def emoji(conn, _params) do
281 emoji =
282 Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
283 Map.put(acc, code, %{image_url: file, tags: tags})
284 end)
285
286 json(conn, emoji)
287 end
288
289 def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
290 with {:ok, _} <- User.update_notification_settings(user, params) do
291 json(conn, %{status: "success"})
292 end
293 end
294
295 def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
296 follow_import(conn, %{"list" => File.read!(listfile.path)})
297 end
298
299 def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
300 with lines <- String.split(list, "\n"),
301 followed_identifiers <-
302 Enum.map(lines, fn line ->
303 String.split(line, ",") |> List.first()
304 end)
305 |> List.delete("Account address") do
306 User.follow_import(follower, followed_identifiers)
307 json(conn, "job started")
308 end
309 end
310
311 def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
312 blocks_import(conn, %{"list" => File.read!(listfile.path)})
313 end
314
315 def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
316 with blocked_identifiers <- String.split(list) do
317 User.blocks_import(blocker, blocked_identifiers)
318 json(conn, "job started")
319 end
320 end
321
322 def change_password(%{assigns: %{user: user}} = conn, params) do
323 case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
324 {:ok, user} ->
325 with {:ok, _user} <-
326 User.reset_password(user, %{
327 password: params["new_password"],
328 password_confirmation: params["new_password_confirmation"]
329 }) do
330 json(conn, %{status: "success"})
331 else
332 {:error, changeset} ->
333 {_, {error, _}} = Enum.at(changeset.errors, 0)
334 json(conn, %{error: "New password #{error}."})
335
336 _ ->
337 json(conn, %{error: "Unable to change password."})
338 end
339
340 {:error, msg} ->
341 json(conn, %{error: msg})
342 end
343 end
344
345 def change_email(%{assigns: %{user: user}} = conn, params) do
346 case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
347 {:ok, user} ->
348 with {:ok, _user} <- User.change_email(user, params["email"]) do
349 json(conn, %{status: "success"})
350 else
351 {:error, changeset} ->
352 {_, {error, _}} = Enum.at(changeset.errors, 0)
353 json(conn, %{error: "Email #{error}."})
354
355 _ ->
356 json(conn, %{error: "Unable to change email."})
357 end
358
359 {:error, msg} ->
360 json(conn, %{error: msg})
361 end
362 end
363
364 def delete_account(%{assigns: %{user: user}} = conn, params) do
365 password = params["password"] || ""
366
367 case CommonAPI.Utils.confirm_current_password(user, password) do
368 {:ok, user} ->
369 User.delete(user)
370 json(conn, %{status: "success"})
371
372 {:error, msg} ->
373 json(conn, %{error: msg})
374 end
375 end
376
377 def disable_account(%{assigns: %{user: user}} = conn, params) do
378 case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
379 {:ok, user} ->
380 User.deactivate_async(user)
381 json(conn, %{status: "success"})
382
383 {:error, msg} ->
384 json(conn, %{error: msg})
385 end
386 end
387
388 def captcha(conn, _params) do
389 json(conn, Pleroma.Captcha.new())
390 end
391
392 def healthcheck(conn, _params) do
393 with true <- Config.get([:instance, :healthcheck]),
394 %{healthy: true} = info <- Healthcheck.system_info() do
395 json(conn, info)
396 else
397 %{healthy: false} = info ->
398 service_unavailable(conn, info)
399
400 _ ->
401 service_unavailable(conn, %{})
402 end
403 end
404
405 defp service_unavailable(conn, info) do
406 conn
407 |> put_status(:service_unavailable)
408 |> json(info)
409 end
410 end