Merge remote-tracking branch 'origin/develop' into reactions
[akkoma] / lib / pleroma / web / mastodon_api / controllers / mastodon_api_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.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
10
11 alias Ecto.Changeset
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
14 alias Pleroma.Config
15 alias Pleroma.Emoji
16 alias Pleroma.HTTP
17 alias Pleroma.Object
18 alias Pleroma.Pagination
19 alias Pleroma.Plugs.RateLimiter
20 alias Pleroma.Repo
21 alias Pleroma.Stats
22 alias Pleroma.User
23 alias Pleroma.Web
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ListView
30 alias Pleroma.Web.MastodonAPI.MastodonAPI
31 alias Pleroma.Web.MastodonAPI.MastodonView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Scopes
37 alias Pleroma.Web.OAuth.Token
38 alias Pleroma.Web.TwitterAPI.TwitterAPI
39
40 require Logger
41 require Pleroma.Constants
42
43 @rate_limited_relations_actions ~w(follow unfollow)a
44
45 plug(
46 RateLimiter,
47 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
48 )
49
50 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
51 plug(RateLimiter, :app_account_creation when action == :account_register)
52 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
53 plug(RateLimiter, :password_reset when action == :password_reset)
54 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
55
56 @local_mastodon_name "Mastodon-Local"
57
58 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
59
60 def create_app(conn, params) do
61 scopes = Scopes.fetch_scopes(params, ["read"])
62
63 app_attrs =
64 params
65 |> Map.drop(["scope", "scopes"])
66 |> Map.put("scopes", scopes)
67
68 with cs <- App.register_changeset(%App{}, app_attrs),
69 false <- cs.changes[:client_name] == @local_mastodon_name,
70 {:ok, app} <- Repo.insert(cs) do
71 conn
72 |> put_view(AppView)
73 |> render("show.json", %{app: app})
74 end
75 end
76
77 defp add_if_present(
78 map,
79 params,
80 params_field,
81 map_field,
82 value_function \\ fn x -> {:ok, x} end
83 ) do
84 if Map.has_key?(params, params_field) do
85 case value_function.(params[params_field]) do
86 {:ok, new_value} -> Map.put(map, map_field, new_value)
87 :error -> map
88 end
89 else
90 map
91 end
92 end
93
94 def update_credentials(%{assigns: %{user: user}} = conn, params) do
95 original_user = user
96
97 user_params =
98 %{}
99 |> add_if_present(params, "display_name", :name)
100 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
101 |> add_if_present(params, "avatar", :avatar, fn value ->
102 with %Plug.Upload{} <- value,
103 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
104 {:ok, object.data}
105 else
106 _ -> :error
107 end
108 end)
109
110 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
111
112 user_info_emojis =
113 user.info
114 |> Map.get(:emoji, [])
115 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
116 |> Enum.dedup()
117
118 info_params =
119 [
120 :no_rich_text,
121 :locked,
122 :hide_followers_count,
123 :hide_follows_count,
124 :hide_followers,
125 :hide_follows,
126 :hide_favorites,
127 :show_role,
128 :skip_thread_containment,
129 :discoverable
130 ]
131 |> Enum.reduce(%{}, fn key, acc ->
132 add_if_present(acc, params, to_string(key), key, fn value ->
133 {:ok, truthy_param?(value)}
134 end)
135 end)
136 |> add_if_present(params, "default_scope", :default_scope)
137 |> add_if_present(params, "fields", :fields, fn fields ->
138 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
139
140 {:ok, fields}
141 end)
142 |> add_if_present(params, "fields", :raw_fields)
143 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
144 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
145 end)
146 |> add_if_present(params, "header", :banner, fn value ->
147 with %Plug.Upload{} <- value,
148 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
149 {:ok, object.data}
150 else
151 _ -> :error
152 end
153 end)
154 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
155 with %Plug.Upload{} <- value,
156 {:ok, object} <- ActivityPub.upload(value, type: :background) do
157 {:ok, object.data}
158 else
159 _ -> :error
160 end
161 end)
162 |> Map.put(:emoji, user_info_emojis)
163
164 changeset =
165 user
166 |> User.update_changeset(user_params)
167 |> User.change_info(&User.Info.profile_update(&1, info_params))
168
169 with {:ok, user} <- User.update_and_set_cache(changeset) do
170 if original_user != user, do: CommonAPI.update(user)
171
172 json(
173 conn,
174 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
175 )
176 else
177 _e -> render_error(conn, :forbidden, "Invalid request")
178 end
179 end
180
181 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
182 change = Changeset.change(user, %{avatar: nil})
183 {:ok, user} = User.update_and_set_cache(change)
184 CommonAPI.update(user)
185
186 json(conn, %{url: nil})
187 end
188
189 def update_avatar(%{assigns: %{user: user}} = conn, params) do
190 {:ok, object} = ActivityPub.upload(params, type: :avatar)
191 change = Changeset.change(user, %{avatar: object.data})
192 {:ok, user} = User.update_and_set_cache(change)
193 CommonAPI.update(user)
194 %{"url" => [%{"href" => href} | _]} = object.data
195
196 json(conn, %{url: href})
197 end
198
199 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
200 new_info = %{"banner" => %{}}
201
202 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
203 CommonAPI.update(user)
204 json(conn, %{url: nil})
205 end
206 end
207
208 def update_banner(%{assigns: %{user: user}} = conn, params) do
209 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
210 new_info <- %{"banner" => object.data},
211 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
212 CommonAPI.update(user)
213 %{"url" => [%{"href" => href} | _]} = object.data
214
215 json(conn, %{url: href})
216 end
217 end
218
219 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
220 new_info = %{"background" => %{}}
221
222 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
223 json(conn, %{url: nil})
224 end
225 end
226
227 def update_background(%{assigns: %{user: user}} = conn, params) do
228 with {:ok, object} <- ActivityPub.upload(params, type: :background),
229 new_info <- %{"background" => object.data},
230 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
231 %{"url" => [%{"href" => href} | _]} = object.data
232
233 json(conn, %{url: href})
234 end
235 end
236
237 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
238 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
239
240 account =
241 AccountView.render("account.json", %{
242 user: user,
243 for: user,
244 with_pleroma_settings: true,
245 with_chat_token: chat_token
246 })
247
248 json(conn, account)
249 end
250
251 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
252 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
253 conn
254 |> put_view(AppView)
255 |> render("short.json", %{app: app})
256 end
257 end
258
259 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
260 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
261 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
262 account = AccountView.render("account.json", %{user: user, for: for_user})
263 json(conn, account)
264 else
265 _e -> render_error(conn, :not_found, "Can't find user")
266 end
267 end
268
269 @mastodon_api_level "2.7.2"
270
271 def masto_instance(conn, _params) do
272 instance = Config.get(:instance)
273
274 response = %{
275 uri: Web.base_url(),
276 title: Keyword.get(instance, :name),
277 description: Keyword.get(instance, :description),
278 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
279 email: Keyword.get(instance, :email),
280 urls: %{
281 streaming_api: Pleroma.Web.Endpoint.websocket_url()
282 },
283 stats: Stats.get_stats(),
284 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
285 languages: ["en"],
286 registrations: Pleroma.Config.get([:instance, :registrations_open]),
287 # Extra (not present in Mastodon):
288 max_toot_chars: Keyword.get(instance, :limit),
289 poll_limits: Keyword.get(instance, :poll_limits)
290 }
291
292 json(conn, response)
293 end
294
295 def peers(conn, _params) do
296 json(conn, Stats.get_peers())
297 end
298
299 defp mastodonized_emoji do
300 Pleroma.Emoji.get_all()
301 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
302 url = to_string(URI.merge(Web.base_url(), relative_url))
303
304 %{
305 "shortcode" => shortcode,
306 "static_url" => url,
307 "visible_in_picker" => true,
308 "url" => url,
309 "tags" => tags,
310 # Assuming that a comma is authorized in the category name
311 "category" => (tags -- ["Custom"]) |> Enum.join(",")
312 }
313 end)
314 end
315
316 def custom_emojis(conn, _params) do
317 mastodon_emoji = mastodonized_emoji()
318 json(conn, mastodon_emoji)
319 end
320
321 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
322 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
323 params =
324 params
325 |> Map.put("tag", params["tagged"])
326
327 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
328
329 conn
330 |> add_link_headers(activities)
331 |> put_view(StatusView)
332 |> render("index.json", %{
333 activities: activities,
334 for: reading_user,
335 as: :activity
336 })
337 end
338 end
339
340 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
342 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
343 true <- Visibility.visible_for_user?(activity, user) do
344 conn
345 |> put_view(StatusView)
346 |> try_render("poll.json", %{object: object, for: user})
347 else
348 error when is_nil(error) or error == false ->
349 render_error(conn, :not_found, "Record not found")
350 end
351 end
352
353 defp get_cached_vote_or_vote(user, object, choices) do
354 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
355
356 {_, res} =
357 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
358 case CommonAPI.vote(user, object, choices) do
359 {:error, _message} = res -> {:ignore, res}
360 res -> {:commit, res}
361 end
362 end)
363
364 res
365 end
366
367 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
368 with %Object{} = object <- Object.get_by_id(id),
369 true <- object.data["type"] == "Question",
370 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
371 true <- Visibility.visible_for_user?(activity, user),
372 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
373 conn
374 |> put_view(StatusView)
375 |> try_render("poll.json", %{object: object, for: user})
376 else
377 nil ->
378 render_error(conn, :not_found, "Record not found")
379
380 false ->
381 render_error(conn, :not_found, "Record not found")
382
383 {:error, message} ->
384 conn
385 |> put_status(:unprocessable_entity)
386 |> json(%{error: message})
387 end
388 end
389
390 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
391 targets = User.get_all_by_ids(List.wrap(id))
392
393 conn
394 |> put_view(AccountView)
395 |> render("relationships.json", %{user: user, targets: targets})
396 end
397
398 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
399 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
400
401 def update_media(
402 %{assigns: %{user: user}} = conn,
403 %{"id" => id, "description" => description} = _
404 )
405 when is_binary(description) do
406 with %Object{} = object <- Repo.get(Object, id),
407 true <- Object.authorize_mutation(object, user),
408 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
409 attachment_data = Map.put(data, "id", object.id)
410
411 conn
412 |> put_view(StatusView)
413 |> render("attachment.json", %{attachment: attachment_data})
414 end
415 end
416
417 def update_media(_conn, _data), do: {:error, :bad_request}
418
419 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
420 with {:ok, object} <-
421 ActivityPub.upload(
422 file,
423 actor: User.ap_id(user),
424 description: Map.get(data, "description")
425 ) do
426 attachment_data = Map.put(object.data, "id", object.id)
427
428 conn
429 |> put_view(StatusView)
430 |> render("attachment.json", %{attachment: attachment_data})
431 end
432 end
433
434 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
435 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
436 %{} = attachment_data <- Map.put(object.data, "id", object.id),
437 # Reject if not an image
438 %{type: "image"} = rendered <-
439 StatusView.render("attachment.json", %{attachment: attachment_data}) do
440 # Sure!
441 # Save to the user's info
442 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
443
444 json(conn, rendered)
445 else
446 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
447 end
448 end
449
450 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
451 mascot = User.get_mascot(user)
452
453 json(conn, mascot)
454 end
455
456 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
457 with %User{} = user <- User.get_cached_by_id(id),
458 followers <- MastodonAPI.get_followers(user, params) do
459 followers =
460 cond do
461 for_user && user.id == for_user.id -> followers
462 user.info.hide_followers -> []
463 true -> followers
464 end
465
466 conn
467 |> add_link_headers(followers)
468 |> put_view(AccountView)
469 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
470 end
471 end
472
473 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
474 with %User{} = user <- User.get_cached_by_id(id),
475 followers <- MastodonAPI.get_friends(user, params) do
476 followers =
477 cond do
478 for_user && user.id == for_user.id -> followers
479 user.info.hide_follows -> []
480 true -> followers
481 end
482
483 conn
484 |> add_link_headers(followers)
485 |> put_view(AccountView)
486 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
487 end
488 end
489
490 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
491 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
492 {_, true} <- {:followed, follower.id != followed.id},
493 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
494 conn
495 |> put_view(AccountView)
496 |> render("relationship.json", %{user: follower, target: followed})
497 else
498 {:followed, _} ->
499 {:error, :not_found}
500
501 {:error, message} ->
502 conn
503 |> put_status(:forbidden)
504 |> json(%{error: message})
505 end
506 end
507
508 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
509 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
510 {_, true} <- {:followed, follower.id != followed.id},
511 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
512 conn
513 |> put_view(AccountView)
514 |> render("account.json", %{user: followed, for: follower})
515 else
516 {:followed, _} ->
517 {:error, :not_found}
518
519 {:error, message} ->
520 conn
521 |> put_status(:forbidden)
522 |> json(%{error: message})
523 end
524 end
525
526 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
527 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
528 {_, true} <- {:followed, follower.id != followed.id},
529 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
530 conn
531 |> put_view(AccountView)
532 |> render("relationship.json", %{user: follower, target: followed})
533 else
534 {:followed, _} ->
535 {:error, :not_found}
536
537 error ->
538 error
539 end
540 end
541
542 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
543 notifications =
544 if Map.has_key?(params, "notifications"),
545 do: params["notifications"] in [true, "True", "true", "1"],
546 else: true
547
548 with %User{} = muted <- User.get_cached_by_id(id),
549 {:ok, muter} <- User.mute(muter, muted, notifications) do
550 conn
551 |> put_view(AccountView)
552 |> render("relationship.json", %{user: muter, target: muted})
553 else
554 {:error, message} ->
555 conn
556 |> put_status(:forbidden)
557 |> json(%{error: message})
558 end
559 end
560
561 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
562 with %User{} = muted <- User.get_cached_by_id(id),
563 {:ok, muter} <- User.unmute(muter, muted) do
564 conn
565 |> put_view(AccountView)
566 |> render("relationship.json", %{user: muter, target: muted})
567 else
568 {:error, message} ->
569 conn
570 |> put_status(:forbidden)
571 |> json(%{error: message})
572 end
573 end
574
575 def mutes(%{assigns: %{user: user}} = conn, _) do
576 with muted_accounts <- User.muted_users(user) do
577 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
578 json(conn, res)
579 end
580 end
581
582 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
583 with %User{} = blocked <- User.get_cached_by_id(id),
584 {:ok, blocker} <- User.block(blocker, blocked),
585 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
586 conn
587 |> put_view(AccountView)
588 |> render("relationship.json", %{user: blocker, target: blocked})
589 else
590 {:error, message} ->
591 conn
592 |> put_status(:forbidden)
593 |> json(%{error: message})
594 end
595 end
596
597 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
598 with %User{} = blocked <- User.get_cached_by_id(id),
599 {:ok, blocker} <- User.unblock(blocker, blocked),
600 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
601 conn
602 |> put_view(AccountView)
603 |> render("relationship.json", %{user: blocker, target: blocked})
604 else
605 {:error, message} ->
606 conn
607 |> put_status(:forbidden)
608 |> json(%{error: message})
609 end
610 end
611
612 def blocks(%{assigns: %{user: user}} = conn, _) do
613 with blocked_accounts <- User.blocked_users(user) do
614 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
615 json(conn, res)
616 end
617 end
618
619 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
620 with %User{} = subscription_target <- User.get_cached_by_id(id),
621 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
622 conn
623 |> put_view(AccountView)
624 |> render("relationship.json", %{user: user, target: subscription_target})
625 else
626 nil -> {:error, :not_found}
627 e -> e
628 end
629 end
630
631 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
632 with %User{} = subscription_target <- User.get_cached_by_id(id),
633 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
634 conn
635 |> put_view(AccountView)
636 |> render("relationship.json", %{user: user, target: subscription_target})
637 else
638 nil -> {:error, :not_found}
639 e -> e
640 end
641 end
642
643 def favourites(%{assigns: %{user: user}} = conn, params) do
644 params =
645 params
646 |> Map.put("type", "Create")
647 |> Map.put("favorited_by", user.ap_id)
648 |> Map.put("blocking_user", user)
649
650 activities =
651 ActivityPub.fetch_activities([], params)
652 |> Enum.reverse()
653
654 conn
655 |> add_link_headers(activities)
656 |> put_view(StatusView)
657 |> render("index.json", %{activities: activities, for: user, as: :activity})
658 end
659
660 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
661 with %User{} = user <- User.get_by_id(id),
662 false <- user.info.hide_favorites do
663 params =
664 params
665 |> Map.put("type", "Create")
666 |> Map.put("favorited_by", user.ap_id)
667 |> Map.put("blocking_user", for_user)
668
669 recipients =
670 if for_user do
671 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
672 else
673 [Pleroma.Constants.as_public()]
674 end
675
676 activities =
677 recipients
678 |> ActivityPub.fetch_activities(params)
679 |> Enum.reverse()
680
681 conn
682 |> add_link_headers(activities)
683 |> put_view(StatusView)
684 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
685 else
686 nil -> {:error, :not_found}
687 true -> render_error(conn, :forbidden, "Can't get favorites")
688 end
689 end
690
691 def bookmarks(%{assigns: %{user: user}} = conn, params) do
692 user = User.get_cached_by_id(user.id)
693
694 bookmarks =
695 Bookmark.for_user_query(user.id)
696 |> Pagination.fetch_paginated(params)
697
698 activities =
699 bookmarks
700 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
701
702 conn
703 |> add_link_headers(bookmarks)
704 |> put_view(StatusView)
705 |> render("index.json", %{activities: activities, for: user, as: :activity})
706 end
707
708 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
709 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
710
711 conn
712 |> put_view(ListView)
713 |> render("index.json", %{lists: lists})
714 end
715
716 def index(%{assigns: %{user: user}} = conn, _params) do
717 token = get_session(conn, :oauth_token)
718
719 if user && token do
720 mastodon_emoji = mastodonized_emoji()
721
722 limit = Config.get([:instance, :limit])
723
724 accounts =
725 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
726
727 initial_state =
728 %{
729 meta: %{
730 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
731 access_token: token,
732 locale: "en",
733 domain: Pleroma.Web.Endpoint.host(),
734 admin: "1",
735 me: "#{user.id}",
736 unfollow_modal: false,
737 boost_modal: false,
738 delete_modal: true,
739 auto_play_gif: false,
740 display_sensitive_media: false,
741 reduce_motion: false,
742 max_toot_chars: limit,
743 mascot: User.get_mascot(user)["url"]
744 },
745 poll_limits: Config.get([:instance, :poll_limits]),
746 rights: %{
747 delete_others_notice: present?(user.info.is_moderator),
748 admin: present?(user.info.is_admin)
749 },
750 compose: %{
751 me: "#{user.id}",
752 default_privacy: user.info.default_scope,
753 default_sensitive: false,
754 allow_content_types: Config.get([:instance, :allowed_post_formats])
755 },
756 media_attachments: %{
757 accept_content_types: [
758 ".jpg",
759 ".jpeg",
760 ".png",
761 ".gif",
762 ".webm",
763 ".mp4",
764 ".m4v",
765 "image\/jpeg",
766 "image\/png",
767 "image\/gif",
768 "video\/webm",
769 "video\/mp4"
770 ]
771 },
772 settings:
773 user.info.settings ||
774 %{
775 onboarded: true,
776 home: %{
777 shows: %{
778 reblog: true,
779 reply: true
780 }
781 },
782 notifications: %{
783 alerts: %{
784 follow: true,
785 favourite: true,
786 reblog: true,
787 mention: true
788 },
789 shows: %{
790 follow: true,
791 favourite: true,
792 reblog: true,
793 mention: true
794 },
795 sounds: %{
796 follow: true,
797 favourite: true,
798 reblog: true,
799 mention: true
800 }
801 }
802 },
803 push_subscription: nil,
804 accounts: accounts,
805 custom_emojis: mastodon_emoji,
806 char_limit: limit
807 }
808 |> Jason.encode!()
809
810 conn
811 |> put_layout(false)
812 |> put_view(MastodonView)
813 |> render("index.html", %{initial_state: initial_state})
814 else
815 conn
816 |> put_session(:return_to, conn.request_path)
817 |> redirect(to: "/web/login")
818 end
819 end
820
821 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
822 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
823 json(conn, %{})
824 else
825 e ->
826 conn
827 |> put_status(:internal_server_error)
828 |> json(%{error: inspect(e)})
829 end
830 end
831
832 def login(%{assigns: %{user: %User{}}} = conn, _params) do
833 redirect(conn, to: local_mastodon_root_path(conn))
834 end
835
836 @doc "Local Mastodon FE login init action"
837 def login(conn, %{"code" => auth_token}) do
838 with {:ok, app} <- get_or_make_app(),
839 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
840 {:ok, token} <- Token.exchange_token(app, auth) do
841 conn
842 |> put_session(:oauth_token, token.token)
843 |> redirect(to: local_mastodon_root_path(conn))
844 end
845 end
846
847 @doc "Local Mastodon FE callback action"
848 def login(conn, _) do
849 with {:ok, app} <- get_or_make_app() do
850 path =
851 o_auth_path(conn, :authorize,
852 response_type: "code",
853 client_id: app.client_id,
854 redirect_uri: ".",
855 scope: Enum.join(app.scopes, " ")
856 )
857
858 redirect(conn, to: path)
859 end
860 end
861
862 defp local_mastodon_root_path(conn) do
863 case get_session(conn, :return_to) do
864 nil ->
865 mastodon_api_path(conn, :index, ["getting-started"])
866
867 return_to ->
868 delete_session(conn, :return_to)
869 return_to
870 end
871 end
872
873 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
874 defp get_or_make_app do
875 App.get_or_make(
876 %{client_name: @local_mastodon_name, redirect_uris: "."},
877 ["read", "write", "follow", "push"]
878 )
879 end
880
881 def logout(conn, _) do
882 conn
883 |> clear_session
884 |> redirect(to: "/")
885 end
886
887 # Stubs for unimplemented mastodon api
888 #
889 def empty_array(conn, _) do
890 Logger.debug("Unimplemented, returning an empty array")
891 json(conn, [])
892 end
893
894 def empty_object(conn, _) do
895 Logger.debug("Unimplemented, returning an empty object")
896 json(conn, %{})
897 end
898
899 def suggestions(%{assigns: %{user: user}} = conn, _) do
900 suggestions = Config.get(:suggestions)
901
902 if Keyword.get(suggestions, :enabled, false) do
903 api = Keyword.get(suggestions, :third_party_engine, "")
904 timeout = Keyword.get(suggestions, :timeout, 5000)
905 limit = Keyword.get(suggestions, :limit, 23)
906
907 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
908
909 user = user.nickname
910
911 url =
912 api
913 |> String.replace("{{host}}", host)
914 |> String.replace("{{user}}", user)
915
916 with {:ok, %{status: 200, body: body}} <-
917 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
918 {:ok, data} <- Jason.decode(body) do
919 data =
920 data
921 |> Enum.slice(0, limit)
922 |> Enum.map(fn x ->
923 x
924 |> Map.put("id", fetch_suggestion_id(x))
925 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
926 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
927 end)
928
929 json(conn, data)
930 else
931 e ->
932 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
933 end
934 else
935 json(conn, [])
936 end
937 end
938
939 defp fetch_suggestion_id(attrs) do
940 case User.get_or_fetch(attrs["acct"]) do
941 {:ok, %User{id: id}} -> id
942 _ -> 0
943 end
944 end
945
946 def account_register(
947 %{assigns: %{app: app}} = conn,
948 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
949 ) do
950 params =
951 params
952 |> Map.take([
953 "email",
954 "captcha_solution",
955 "captcha_token",
956 "captcha_answer_data",
957 "token",
958 "password"
959 ])
960 |> Map.put("nickname", nickname)
961 |> Map.put("fullname", params["fullname"] || nickname)
962 |> Map.put("bio", params["bio"] || "")
963 |> Map.put("confirm", params["password"])
964
965 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
966 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
967 json(conn, %{
968 token_type: "Bearer",
969 access_token: token.token,
970 scope: app.scopes,
971 created_at: Token.Utils.format_created_at(token)
972 })
973 else
974 {:error, errors} ->
975 conn
976 |> put_status(:bad_request)
977 |> json(errors)
978 end
979 end
980
981 def account_register(%{assigns: %{app: _app}} = conn, _) do
982 render_error(conn, :bad_request, "Missing parameters")
983 end
984
985 def account_register(conn, _) do
986 render_error(conn, :forbidden, "Invalid credentials")
987 end
988
989 def password_reset(conn, params) do
990 nickname_or_email = params["email"] || params["nickname"]
991
992 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
993 conn
994 |> put_status(:no_content)
995 |> json("")
996 else
997 {:error, "unknown user"} ->
998 send_resp(conn, :not_found, "")
999
1000 {:error, _} ->
1001 send_resp(conn, :bad_request, "")
1002 end
1003 end
1004
1005 def account_confirmation_resend(conn, params) do
1006 nickname_or_email = params["email"] || params["nickname"]
1007
1008 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1009 {:ok, _} <- User.try_send_confirmation_email(user) do
1010 conn
1011 |> json_response(:no_content, "")
1012 end
1013 end
1014
1015 def try_render(conn, target, params)
1016 when is_binary(target) do
1017 case render(conn, target, params) do
1018 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1019 res -> res
1020 end
1021 end
1022
1023 def try_render(conn, _, _) do
1024 render_error(conn, :not_implemented, "Can't display this activity")
1025 end
1026
1027 defp present?(nil), do: false
1028 defp present?(false), do: false
1029 defp present?(_), do: true
1030 end