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