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