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