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