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