e96bf6fd9e2e39cf46c3c7555fdca2c67086cd25
[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 subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
719 with %User{} = subscription_target <- User.get_cached_by_id(id),
720 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
721 conn
722 |> put_view(AccountView)
723 |> render("relationship.json", %{user: user, target: subscription_target})
724 else
725 {:error, message} ->
726 conn
727 |> put_status(:forbidden)
728 |> json(%{error: message})
729 end
730 end
731
732 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
733 with %User{} = subscription_target <- User.get_cached_by_id(id),
734 {:ok, subscription_target} = User.unsubscribe(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 favourites(%{assigns: %{user: user}} = conn, params) do
747 params =
748 params
749 |> Map.put("type", "Create")
750 |> Map.put("favorited_by", user.ap_id)
751 |> Map.put("blocking_user", user)
752
753 activities =
754 ActivityPub.fetch_activities([], params)
755 |> Enum.reverse()
756
757 conn
758 |> add_link_headers(activities)
759 |> put_view(StatusView)
760 |> render("index.json", %{activities: activities, for: user, as: :activity})
761 end
762
763 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
764 with %User{} = user <- User.get_by_id(id),
765 false <- user.info.hide_favorites do
766 params =
767 params
768 |> Map.put("type", "Create")
769 |> Map.put("favorited_by", user.ap_id)
770 |> Map.put("blocking_user", for_user)
771
772 recipients =
773 if for_user do
774 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
775 else
776 [Pleroma.Constants.as_public()]
777 end
778
779 activities =
780 recipients
781 |> ActivityPub.fetch_activities(params)
782 |> Enum.reverse()
783
784 conn
785 |> add_link_headers(activities)
786 |> put_view(StatusView)
787 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
788 else
789 nil -> {:error, :not_found}
790 true -> render_error(conn, :forbidden, "Can't get favorites")
791 end
792 end
793
794 def bookmarks(%{assigns: %{user: user}} = conn, params) do
795 user = User.get_cached_by_id(user.id)
796
797 bookmarks =
798 Bookmark.for_user_query(user.id)
799 |> Pagination.fetch_paginated(params)
800
801 activities =
802 bookmarks
803 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
804
805 conn
806 |> add_link_headers(bookmarks)
807 |> put_view(StatusView)
808 |> render("index.json", %{activities: activities, for: user, as: :activity})
809 end
810
811 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
812 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
813 res = ListView.render("lists.json", lists: lists)
814 json(conn, res)
815 end
816
817 def index(%{assigns: %{user: user}} = conn, _params) do
818 token = get_session(conn, :oauth_token)
819
820 if user && token do
821 mastodon_emoji = mastodonized_emoji()
822
823 limit = Config.get([:instance, :limit])
824
825 accounts =
826 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
827
828 initial_state =
829 %{
830 meta: %{
831 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
832 access_token: token,
833 locale: "en",
834 domain: Pleroma.Web.Endpoint.host(),
835 admin: "1",
836 me: "#{user.id}",
837 unfollow_modal: false,
838 boost_modal: false,
839 delete_modal: true,
840 auto_play_gif: false,
841 display_sensitive_media: false,
842 reduce_motion: false,
843 max_toot_chars: limit,
844 mascot: User.get_mascot(user)["url"]
845 },
846 poll_limits: Config.get([:instance, :poll_limits]),
847 rights: %{
848 delete_others_notice: present?(user.info.is_moderator),
849 admin: present?(user.info.is_admin)
850 },
851 compose: %{
852 me: "#{user.id}",
853 default_privacy: user.info.default_scope,
854 default_sensitive: false,
855 allow_content_types: Config.get([:instance, :allowed_post_formats])
856 },
857 media_attachments: %{
858 accept_content_types: [
859 ".jpg",
860 ".jpeg",
861 ".png",
862 ".gif",
863 ".webm",
864 ".mp4",
865 ".m4v",
866 "image\/jpeg",
867 "image\/png",
868 "image\/gif",
869 "video\/webm",
870 "video\/mp4"
871 ]
872 },
873 settings:
874 user.info.settings ||
875 %{
876 onboarded: true,
877 home: %{
878 shows: %{
879 reblog: true,
880 reply: true
881 }
882 },
883 notifications: %{
884 alerts: %{
885 follow: true,
886 favourite: true,
887 reblog: true,
888 mention: true
889 },
890 shows: %{
891 follow: true,
892 favourite: true,
893 reblog: true,
894 mention: true
895 },
896 sounds: %{
897 follow: true,
898 favourite: true,
899 reblog: true,
900 mention: true
901 }
902 }
903 },
904 push_subscription: nil,
905 accounts: accounts,
906 custom_emojis: mastodon_emoji,
907 char_limit: limit
908 }
909 |> Jason.encode!()
910
911 conn
912 |> put_layout(false)
913 |> put_view(MastodonView)
914 |> render("index.html", %{initial_state: initial_state})
915 else
916 conn
917 |> put_session(:return_to, conn.request_path)
918 |> redirect(to: "/web/login")
919 end
920 end
921
922 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
923 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
924 json(conn, %{})
925 else
926 e ->
927 conn
928 |> put_status(:internal_server_error)
929 |> json(%{error: inspect(e)})
930 end
931 end
932
933 def login(%{assigns: %{user: %User{}}} = conn, _params) do
934 redirect(conn, to: local_mastodon_root_path(conn))
935 end
936
937 @doc "Local Mastodon FE login init action"
938 def login(conn, %{"code" => auth_token}) do
939 with {:ok, app} <- get_or_make_app(),
940 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
941 {:ok, token} <- Token.exchange_token(app, auth) do
942 conn
943 |> put_session(:oauth_token, token.token)
944 |> redirect(to: local_mastodon_root_path(conn))
945 end
946 end
947
948 @doc "Local Mastodon FE callback action"
949 def login(conn, _) do
950 with {:ok, app} <- get_or_make_app() do
951 path =
952 o_auth_path(
953 conn,
954 :authorize,
955 response_type: "code",
956 client_id: app.client_id,
957 redirect_uri: ".",
958 scope: Enum.join(app.scopes, " ")
959 )
960
961 redirect(conn, to: path)
962 end
963 end
964
965 defp local_mastodon_root_path(conn) do
966 case get_session(conn, :return_to) do
967 nil ->
968 mastodon_api_path(conn, :index, ["getting-started"])
969
970 return_to ->
971 delete_session(conn, :return_to)
972 return_to
973 end
974 end
975
976 defp get_or_make_app do
977 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
978 scopes = ["read", "write", "follow", "push"]
979
980 with %App{} = app <- Repo.get_by(App, find_attrs) do
981 {:ok, app} =
982 if app.scopes == scopes do
983 {:ok, app}
984 else
985 app
986 |> Changeset.change(%{scopes: scopes})
987 |> Repo.update()
988 end
989
990 {:ok, app}
991 else
992 _e ->
993 cs =
994 App.register_changeset(
995 %App{},
996 Map.put(find_attrs, :scopes, scopes)
997 )
998
999 Repo.insert(cs)
1000 end
1001 end
1002
1003 def logout(conn, _) do
1004 conn
1005 |> clear_session
1006 |> redirect(to: "/")
1007 end
1008
1009 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1010 Logger.debug("Unimplemented, returning unmodified relationship")
1011
1012 with %User{} = target <- User.get_cached_by_id(id) do
1013 conn
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: user, target: target})
1016 end
1017 end
1018
1019 def empty_array(conn, _) do
1020 Logger.debug("Unimplemented, returning an empty array")
1021 json(conn, [])
1022 end
1023
1024 def empty_object(conn, _) do
1025 Logger.debug("Unimplemented, returning an empty object")
1026 json(conn, %{})
1027 end
1028
1029 def get_filters(%{assigns: %{user: user}} = conn, _) do
1030 filters = Filter.get_filters(user)
1031 res = FilterView.render("filters.json", filters: filters)
1032 json(conn, res)
1033 end
1034
1035 def create_filter(
1036 %{assigns: %{user: user}} = conn,
1037 %{"phrase" => phrase, "context" => context} = params
1038 ) do
1039 query = %Filter{
1040 user_id: user.id,
1041 phrase: phrase,
1042 context: context,
1043 hide: Map.get(params, "irreversible", false),
1044 whole_word: Map.get(params, "boolean", true)
1045 # expires_at
1046 }
1047
1048 {:ok, response} = Filter.create(query)
1049 res = FilterView.render("filter.json", filter: response)
1050 json(conn, res)
1051 end
1052
1053 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1054 filter = Filter.get(filter_id, user)
1055 res = FilterView.render("filter.json", filter: filter)
1056 json(conn, res)
1057 end
1058
1059 def update_filter(
1060 %{assigns: %{user: user}} = conn,
1061 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1062 ) do
1063 query = %Filter{
1064 user_id: user.id,
1065 filter_id: filter_id,
1066 phrase: phrase,
1067 context: context,
1068 hide: Map.get(params, "irreversible", nil),
1069 whole_word: Map.get(params, "boolean", true)
1070 # expires_at
1071 }
1072
1073 {:ok, response} = Filter.update(query)
1074 res = FilterView.render("filter.json", filter: response)
1075 json(conn, res)
1076 end
1077
1078 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1079 query = %Filter{
1080 user_id: user.id,
1081 filter_id: filter_id
1082 }
1083
1084 {:ok, _} = Filter.delete(query)
1085 json(conn, %{})
1086 end
1087
1088 def suggestions(%{assigns: %{user: user}} = conn, _) do
1089 suggestions = Config.get(:suggestions)
1090
1091 if Keyword.get(suggestions, :enabled, false) do
1092 api = Keyword.get(suggestions, :third_party_engine, "")
1093 timeout = Keyword.get(suggestions, :timeout, 5000)
1094 limit = Keyword.get(suggestions, :limit, 23)
1095
1096 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1097
1098 user = user.nickname
1099
1100 url =
1101 api
1102 |> String.replace("{{host}}", host)
1103 |> String.replace("{{user}}", user)
1104
1105 with {:ok, %{status: 200, body: body}} <-
1106 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1107 {:ok, data} <- Jason.decode(body) do
1108 data =
1109 data
1110 |> Enum.slice(0, limit)
1111 |> Enum.map(fn x ->
1112 x
1113 |> Map.put("id", fetch_suggestion_id(x))
1114 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1115 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1116 end)
1117
1118 json(conn, data)
1119 else
1120 e ->
1121 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1122 end
1123 else
1124 json(conn, [])
1125 end
1126 end
1127
1128 defp fetch_suggestion_id(attrs) do
1129 case User.get_or_fetch(attrs["acct"]) do
1130 {:ok, %User{id: id}} -> id
1131 _ -> 0
1132 end
1133 end
1134
1135 def reports(%{assigns: %{user: user}} = conn, params) do
1136 case CommonAPI.report(user, params) do
1137 {:ok, activity} ->
1138 conn
1139 |> put_view(ReportView)
1140 |> try_render("report.json", %{activity: activity})
1141
1142 {:error, err} ->
1143 conn
1144 |> put_status(:bad_request)
1145 |> json(%{error: err})
1146 end
1147 end
1148
1149 def account_register(
1150 %{assigns: %{app: app}} = conn,
1151 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1152 ) do
1153 params =
1154 params
1155 |> Map.take([
1156 "email",
1157 "captcha_solution",
1158 "captcha_token",
1159 "captcha_answer_data",
1160 "token",
1161 "password"
1162 ])
1163 |> Map.put("nickname", nickname)
1164 |> Map.put("fullname", params["fullname"] || nickname)
1165 |> Map.put("bio", params["bio"] || "")
1166 |> Map.put("confirm", params["password"])
1167
1168 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1169 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1170 json(conn, %{
1171 token_type: "Bearer",
1172 access_token: token.token,
1173 scope: app.scopes,
1174 created_at: Token.Utils.format_created_at(token)
1175 })
1176 else
1177 {:error, errors} ->
1178 conn
1179 |> put_status(:bad_request)
1180 |> json(errors)
1181 end
1182 end
1183
1184 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1185 render_error(conn, :bad_request, "Missing parameters")
1186 end
1187
1188 def account_register(conn, _) do
1189 render_error(conn, :forbidden, "Invalid credentials")
1190 end
1191
1192 def conversations(%{assigns: %{user: user}} = conn, params) do
1193 participations = Participation.for_user_with_last_activity_id(user, params)
1194
1195 conversations =
1196 Enum.map(participations, fn participation ->
1197 ConversationView.render("participation.json", %{participation: participation, for: user})
1198 end)
1199
1200 conn
1201 |> add_link_headers(participations)
1202 |> json(conversations)
1203 end
1204
1205 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1206 with %Participation{} = participation <-
1207 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1208 {:ok, participation} <- Participation.mark_as_read(participation) do
1209 participation_view =
1210 ConversationView.render("participation.json", %{participation: participation, for: user})
1211
1212 conn
1213 |> json(participation_view)
1214 end
1215 end
1216
1217 def password_reset(conn, params) do
1218 nickname_or_email = params["email"] || params["nickname"]
1219
1220 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1221 conn
1222 |> put_status(:no_content)
1223 |> json("")
1224 else
1225 {:error, "unknown user"} ->
1226 send_resp(conn, :not_found, "")
1227
1228 {:error, _} ->
1229 send_resp(conn, :bad_request, "")
1230 end
1231 end
1232
1233 def account_confirmation_resend(conn, params) do
1234 nickname_or_email = params["email"] || params["nickname"]
1235
1236 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1237 {:ok, _} <- User.try_send_confirmation_email(user) do
1238 conn
1239 |> json_response(:no_content, "")
1240 end
1241 end
1242
1243 def try_render(conn, target, params)
1244 when is_binary(target) do
1245 case render(conn, target, params) do
1246 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1247 res -> res
1248 end
1249 end
1250
1251 def try_render(conn, _, _) do
1252 render_error(conn, :not_implemented, "Can't display this activity")
1253 end
1254
1255 defp present?(nil), do: false
1256 defp present?(false), do: false
1257 defp present?(_), do: true
1258 end