1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7 alias Pleroma.Object.Fetcher
12 alias Pleroma.Notification
14 alias Pleroma.Pagination
16 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.AppView
25 alias Pleroma.Web.MastodonAPI.FilterView
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonView
29 alias Pleroma.Web.MastodonAPI.NotificationView
30 alias Pleroma.Web.MastodonAPI.ReportView
31 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Token
38 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
43 @httpoison Application.get_env(:pleroma, :httpoison)
44 @local_mastodon_name "Mastodon-Local"
46 action_fallback(:errors)
48 def create_app(conn, params) do
49 scopes = oauth_scopes(params, ["read"])
53 |> Map.drop(["scope", "scopes"])
54 |> Map.put("scopes", scopes)
56 with cs <- App.register_changeset(%App{}, app_attrs),
57 false <- cs.changes[:client_name] == @local_mastodon_name,
58 {:ok, app} <- Repo.insert(cs) do
61 |> render("show.json", %{app: app})
70 value_function \\ fn x -> {:ok, x} end
72 if Map.has_key?(params, params_field) do
73 case value_function.(params[params_field]) do
74 {:ok, new_value} -> Map.put(map, map_field, new_value)
82 def update_credentials(%{assigns: %{user: user}} = conn, params) do
87 |> add_if_present(params, "display_name", :name)
88 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
89 |> add_if_present(params, "avatar", :avatar, fn value ->
90 with %Plug.Upload{} <- value,
91 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
100 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
101 |> add_if_present(params, "header", :banner, fn value ->
102 with %Plug.Upload{} <- value,
103 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
110 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
112 with changeset <- User.update_changeset(user, user_params),
113 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
114 {:ok, user} <- User.update_and_set_cache(changeset) do
115 if original_user != user do
116 CommonAPI.update(user)
119 json(conn, AccountView.render("account.json", %{user: user, for: user}))
124 |> json(%{error: "Invalid request"})
128 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
129 account = AccountView.render("account.json", %{user: user, for: user})
133 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
134 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
137 |> render("short.json", %{app: app})
141 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
142 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
143 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
144 account = AccountView.render("account.json", %{user: user, for: for_user})
150 |> json(%{error: "Can't find user"})
154 @mastodon_api_level "2.5.0"
156 def masto_instance(conn, _params) do
157 instance = Config.get(:instance)
161 title: Keyword.get(instance, :name),
162 description: Keyword.get(instance, :description),
163 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
164 email: Keyword.get(instance, :email),
166 streaming_api: Pleroma.Web.Endpoint.websocket_url()
168 stats: Stats.get_stats(),
169 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
171 registrations: Pleroma.Config.get([:instance, :registrations_open]),
172 # Extra (not present in Mastodon):
173 max_toot_chars: Keyword.get(instance, :limit)
179 def peers(conn, _params) do
180 json(conn, Stats.get_peers())
183 defp mastodonized_emoji do
184 Pleroma.Emoji.get_all()
185 |> Enum.map(fn {shortcode, relative_url, tags} ->
186 url = to_string(URI.merge(Web.base_url(), relative_url))
189 "shortcode" => shortcode,
191 "visible_in_picker" => true,
193 "tags" => String.split(tags, ",")
198 def custom_emojis(conn, _params) do
199 mastodon_emoji = mastodonized_emoji()
200 json(conn, mastodon_emoji)
203 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
206 |> Map.drop(["since_id", "max_id", "min_id"])
209 last = List.last(activities)
216 |> Map.get("limit", "20")
217 |> String.to_integer()
220 if length(activities) <= limit do
226 |> Enum.at(limit * -1)
230 {next_url, prev_url} =
234 Pleroma.Web.Endpoint,
237 Map.merge(params, %{max_id: max_id})
240 Pleroma.Web.Endpoint,
243 Map.merge(params, %{min_id: min_id})
249 Pleroma.Web.Endpoint,
251 Map.merge(params, %{max_id: max_id})
254 Pleroma.Web.Endpoint,
256 Map.merge(params, %{min_id: min_id})
262 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
268 def home_timeline(%{assigns: %{user: user}} = conn, params) do
271 |> Map.put("type", ["Create", "Announce"])
272 |> Map.put("blocking_user", user)
273 |> Map.put("muting_user", user)
274 |> Map.put("user", user)
277 [user.ap_id | user.following]
278 |> ActivityPub.fetch_activities(params)
279 |> ActivityPub.contain_timeline(user)
283 |> add_link_headers(:home_timeline, activities)
284 |> put_view(StatusView)
285 |> render("index.json", %{activities: activities, for: user, as: :activity})
288 def public_timeline(%{assigns: %{user: user}} = conn, params) do
289 local_only = params["local"] in [true, "True", "true", "1"]
293 |> Map.put("type", ["Create", "Announce"])
294 |> Map.put("local_only", local_only)
295 |> Map.put("blocking_user", user)
296 |> Map.put("muting_user", user)
297 |> ActivityPub.fetch_public_activities()
301 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
302 |> put_view(StatusView)
303 |> render("index.json", %{activities: activities, for: user, as: :activity})
306 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
307 with %User{} = user <- User.get_by_id(params["id"]) do
308 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
311 |> add_link_headers(:user_statuses, activities, params["id"])
312 |> put_view(StatusView)
313 |> render("index.json", %{
314 activities: activities,
321 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
324 |> Map.put("type", "Create")
325 |> Map.put("blocking_user", user)
326 |> Map.put("user", user)
327 |> Map.put(:visibility, "direct")
331 |> ActivityPub.fetch_activities_query(params)
332 |> Pagination.fetch_paginated(params)
335 |> add_link_headers(:dm_timeline, activities)
336 |> put_view(StatusView)
337 |> render("index.json", %{activities: activities, for: user, as: :activity})
340 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Activity{} = activity <- Activity.get_by_id(id),
342 true <- Visibility.visible_for_user?(activity, user) do
344 |> put_view(StatusView)
345 |> try_render("status.json", %{activity: activity, for: user})
349 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Activity{} = activity <- Activity.get_by_id(id),
352 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
353 "blocking_user" => user,
357 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
359 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
360 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
366 activities: grouped_activities[true] || [],
370 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
375 activities: grouped_activities[false] || [],
379 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
386 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
387 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
389 |> add_link_headers(:scheduled_statuses, scheduled_activities)
390 |> put_view(ScheduledActivityView)
391 |> render("index.json", %{scheduled_activities: scheduled_activities})
395 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
396 with %ScheduledActivity{} = scheduled_activity <-
397 ScheduledActivity.get(user, scheduled_activity_id) do
399 |> put_view(ScheduledActivityView)
400 |> render("show.json", %{scheduled_activity: scheduled_activity})
402 _ -> {:error, :not_found}
406 def update_scheduled_status(
407 %{assigns: %{user: user}} = conn,
408 %{"id" => scheduled_activity_id} = params
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id),
412 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 nil -> {:error, :not_found}
422 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
436 when length(media_ids) > 0 do
439 |> Map.put("status", ".")
441 post_status(conn, params)
444 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
447 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
450 case get_req_header(conn, "idempotency-key") do
452 _ -> Ecto.UUID.generate()
455 scheduled_at = params["scheduled_at"]
457 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
458 with {:ok, scheduled_activity} <-
459 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
465 params = Map.drop(params, ["scheduled_at"])
468 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
469 CommonAPI.post(user, params)
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
485 |> json(%{error: "Can't delete this post"})
489 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
490 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
492 |> put_view(StatusView)
493 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
497 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
498 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
499 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
501 |> put_view(StatusView)
502 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
506 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
508 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
515 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
527 |> put_view(StatusView)
528 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
532 |> put_resp_content_type("application/json")
533 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
537 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
538 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
540 |> put_view(StatusView)
541 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
545 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
546 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
547 %Object{} = object <- Object.normalize(activity),
548 %User{} = user <- User.get_by_nickname(user.nickname),
549 true <- Visibility.visible_for_user?(activity, user),
550 {:ok, user} <- User.bookmark(user, object.data["id"]) do
552 |> put_view(StatusView)
553 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
557 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
558 with %Activity{} = activity <- Activity.get_by_id(id),
559 %User{} = user <- User.get_by_nickname(user.nickname),
560 true <- Visibility.visible_for_user?(activity, user),
561 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
563 |> put_view(StatusView)
564 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
568 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
569 activity = Activity.get_by_id(id)
571 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
573 |> put_view(StatusView)
574 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
578 |> put_resp_content_type("application/json")
579 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
583 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
584 activity = Activity.get_by_id(id)
586 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
588 |> put_view(StatusView)
589 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
593 def notifications(%{assigns: %{user: user}} = conn, params) do
594 notifications = MastodonAPI.get_notifications(user, params)
597 |> add_link_headers(:notifications, notifications)
598 |> put_view(NotificationView)
599 |> render("index.json", %{notifications: notifications, for: user})
602 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
603 with {:ok, notification} <- Notification.get(user, id) do
605 |> put_view(NotificationView)
606 |> render("show.json", %{notification: notification, for: user})
610 |> put_resp_content_type("application/json")
611 |> send_resp(403, Jason.encode!(%{"error" => reason}))
615 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
616 Notification.clear(user)
620 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
621 with {:ok, _notif} <- Notification.dismiss(user, id) do
626 |> put_resp_content_type("application/json")
627 |> send_resp(403, Jason.encode!(%{"error" => reason}))
631 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
632 Notification.destroy_multiple(user, ids)
636 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
638 q = from(u in User, where: u.id in ^id)
639 targets = Repo.all(q)
642 |> put_view(AccountView)
643 |> render("relationships.json", %{user: user, targets: targets})
646 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
647 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
649 def update_media(%{assigns: %{user: user}} = conn, data) do
650 with %Object{} = object <- Repo.get(Object, data["id"]),
651 true <- Object.authorize_mutation(object, user),
652 true <- is_binary(data["description"]),
653 description <- data["description"] do
654 new_data = %{object.data | "name" => description}
658 |> Object.change(%{data: new_data})
661 attachment_data = Map.put(new_data, "id", object.id)
664 |> put_view(StatusView)
665 |> render("attachment.json", %{attachment: attachment_data})
669 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
670 with {:ok, object} <-
673 actor: User.ap_id(user),
674 description: Map.get(data, "description")
676 attachment_data = Map.put(object.data, "id", object.id)
679 |> put_view(StatusView)
680 |> render("attachment.json", %{attachment: attachment_data})
684 def favourited_by(conn, %{"id" => id}) do
685 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
686 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
687 q = from(u in User, where: u.ap_id in ^likes)
691 |> put_view(AccountView)
692 |> render(AccountView, "accounts.json", %{users: users, as: :user})
698 def reblogged_by(conn, %{"id" => id}) do
699 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
700 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
701 q = from(u in User, where: u.ap_id in ^announces)
705 |> put_view(AccountView)
706 |> render("accounts.json", %{users: users, as: :user})
712 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
713 local_only = params["local"] in [true, "True", "true", "1"]
716 [params["tag"], params["any"]]
720 |> Enum.map(&String.downcase(&1))
725 |> Enum.map(&String.downcase(&1))
730 |> Enum.map(&String.downcase(&1))
734 |> Map.put("type", "Create")
735 |> Map.put("local_only", local_only)
736 |> Map.put("blocking_user", user)
737 |> Map.put("muting_user", user)
738 |> Map.put("tag", tags)
739 |> Map.put("tag_all", tag_all)
740 |> Map.put("tag_reject", tag_reject)
741 |> ActivityPub.fetch_public_activities()
745 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
746 |> put_view(StatusView)
747 |> render("index.json", %{activities: activities, for: user, as: :activity})
750 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
751 with %User{} = user <- User.get_by_id(id),
752 followers <- MastodonAPI.get_followers(user, params) do
755 for_user && user.id == for_user.id -> followers
756 user.info.hide_followers -> []
761 |> add_link_headers(:followers, followers, user)
762 |> put_view(AccountView)
763 |> render("accounts.json", %{users: followers, as: :user})
767 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
768 with %User{} = user <- User.get_by_id(id),
769 followers <- MastodonAPI.get_friends(user, params) do
772 for_user && user.id == for_user.id -> followers
773 user.info.hide_follows -> []
778 |> add_link_headers(:following, followers, user)
779 |> put_view(AccountView)
780 |> render("accounts.json", %{users: followers, as: :user})
784 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
785 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
787 |> put_view(AccountView)
788 |> render("accounts.json", %{users: follow_requests, as: :user})
792 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
793 with %User{} = follower <- User.get_by_id(id),
794 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
796 |> put_view(AccountView)
797 |> render("relationship.json", %{user: followed, target: follower})
801 |> put_resp_content_type("application/json")
802 |> send_resp(403, Jason.encode!(%{"error" => message}))
806 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
807 with %User{} = follower <- User.get_by_id(id),
808 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
810 |> put_view(AccountView)
811 |> render("relationship.json", %{user: followed, target: follower})
815 |> put_resp_content_type("application/json")
816 |> send_resp(403, Jason.encode!(%{"error" => message}))
820 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
821 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
822 {_, true} <- {:followed, follower.id != followed.id},
823 false <- User.following?(follower, followed),
824 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
826 |> put_view(AccountView)
827 |> render("relationship.json", %{user: follower, target: followed})
833 followed = User.get_cached_by_id(id)
836 case conn.params["reblogs"] do
837 true -> CommonAPI.show_reblogs(follower, followed)
838 false -> CommonAPI.hide_reblogs(follower, followed)
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: follower, target: followed})
847 |> put_resp_content_type("application/json")
848 |> send_resp(403, Jason.encode!(%{"error" => message}))
852 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
853 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
854 {_, true} <- {:followed, follower.id != followed.id},
855 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
857 |> put_view(AccountView)
858 |> render("account.json", %{user: followed, for: follower})
865 |> put_resp_content_type("application/json")
866 |> send_resp(403, Jason.encode!(%{"error" => message}))
870 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
871 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
872 {_, true} <- {:followed, follower.id != followed.id},
873 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
875 |> put_view(AccountView)
876 |> render("relationship.json", %{user: follower, target: followed})
886 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
887 with %User{} = muted <- User.get_by_id(id),
888 {:ok, muter} <- User.mute(muter, muted) do
890 |> put_view(AccountView)
891 |> render("relationship.json", %{user: muter, target: muted})
895 |> put_resp_content_type("application/json")
896 |> send_resp(403, Jason.encode!(%{"error" => message}))
900 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
901 with %User{} = muted <- User.get_by_id(id),
902 {:ok, muter} <- User.unmute(muter, muted) do
904 |> put_view(AccountView)
905 |> render("relationship.json", %{user: muter, target: muted})
909 |> put_resp_content_type("application/json")
910 |> send_resp(403, Jason.encode!(%{"error" => message}))
914 def mutes(%{assigns: %{user: user}} = conn, _) do
915 with muted_accounts <- User.muted_users(user) do
916 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
921 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
922 with %User{} = blocked <- User.get_by_id(id),
923 {:ok, blocker} <- User.block(blocker, blocked),
924 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
926 |> put_view(AccountView)
927 |> render("relationship.json", %{user: blocker, target: blocked})
931 |> put_resp_content_type("application/json")
932 |> send_resp(403, Jason.encode!(%{"error" => message}))
936 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
937 with %User{} = blocked <- User.get_by_id(id),
938 {:ok, blocker} <- User.unblock(blocker, blocked),
939 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
941 |> put_view(AccountView)
942 |> render("relationship.json", %{user: blocker, target: blocked})
946 |> put_resp_content_type("application/json")
947 |> send_resp(403, Jason.encode!(%{"error" => message}))
951 def blocks(%{assigns: %{user: user}} = conn, _) do
952 with blocked_accounts <- User.blocked_users(user) do
953 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
958 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
959 json(conn, info.domain_blocks || [])
962 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
963 User.block_domain(blocker, domain)
967 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
968 User.unblock_domain(blocker, domain)
972 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
973 with %User{} = subscription_target <- User.get_cached_by_id(id),
974 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: user, target: subscription_target})
981 |> put_resp_content_type("application/json")
982 |> send_resp(403, Jason.encode!(%{"error" => message}))
986 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
987 with %User{} = subscription_target <- User.get_cached_by_id(id),
988 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
990 |> put_view(AccountView)
991 |> render("relationship.json", %{user: user, target: subscription_target})
995 |> put_resp_content_type("application/json")
996 |> send_resp(403, Jason.encode!(%{"error" => message}))
1000 def status_search(user, query) do
1002 if Regex.match?(~r/https?:/, query) do
1003 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1004 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1005 true <- Visibility.visible_for_user?(activity, user) do
1015 where: fragment("?->>'type' = 'Create'", a.data),
1016 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1019 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
1024 order_by: [desc: :id]
1027 Repo.all(q) ++ fetched
1030 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1031 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1033 statuses = status_search(user, query)
1035 tags_path = Web.base_url() <> "/tag/"
1041 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1042 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1043 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1046 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1048 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1055 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1056 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1058 statuses = status_search(user, query)
1064 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1065 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1068 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1070 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1077 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1078 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1080 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1085 def favourites(%{assigns: %{user: user}} = conn, params) do
1088 |> Map.put("type", "Create")
1089 |> Map.put("favorited_by", user.ap_id)
1090 |> Map.put("blocking_user", user)
1093 ActivityPub.fetch_activities([], params)
1097 |> add_link_headers(:favourites, activities)
1098 |> put_view(StatusView)
1099 |> render("index.json", %{activities: activities, for: user, as: :activity})
1102 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1103 user = User.get_by_id(user.id)
1107 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1111 |> put_view(StatusView)
1112 |> render("index.json", %{activities: activities, for: user, as: :activity})
1115 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1116 lists = Pleroma.List.for_user(user, opts)
1117 res = ListView.render("lists.json", lists: lists)
1121 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1122 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1123 res = ListView.render("list.json", list: list)
1129 |> json(%{error: "Record not found"})
1133 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1134 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1135 res = ListView.render("lists.json", lists: lists)
1139 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1140 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1141 {:ok, _list} <- Pleroma.List.delete(list) do
1149 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1150 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1151 res = ListView.render("list.json", list: list)
1156 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1158 |> Enum.each(fn account_id ->
1159 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1160 %User{} = followed <- User.get_by_id(account_id) do
1161 Pleroma.List.follow(list, followed)
1168 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1170 |> Enum.each(fn account_id ->
1171 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1172 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1173 Pleroma.List.unfollow(list, followed)
1180 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1181 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1182 {:ok, users} = Pleroma.List.get_following(list) do
1184 |> put_view(AccountView)
1185 |> render("accounts.json", %{users: users, as: :user})
1189 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1190 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1191 {:ok, list} <- Pleroma.List.rename(list, title) do
1192 res = ListView.render("list.json", list: list)
1200 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1201 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1204 |> Map.put("type", "Create")
1205 |> Map.put("blocking_user", user)
1206 |> Map.put("muting_user", user)
1208 # we must filter the following list for the user to avoid leaking statuses the user
1209 # does not actually have permission to see (for more info, peruse security issue #270).
1212 |> Enum.filter(fn x -> x in user.following end)
1213 |> ActivityPub.fetch_activities_bounded(following, params)
1217 |> put_view(StatusView)
1218 |> render("index.json", %{activities: activities, for: user, as: :activity})
1223 |> json(%{error: "Error."})
1227 def index(%{assigns: %{user: user}} = conn, _params) do
1228 token = get_session(conn, :oauth_token)
1231 mastodon_emoji = mastodonized_emoji()
1233 limit = Config.get([:instance, :limit])
1236 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1238 flavour = get_user_flavour(user)
1243 streaming_api_base_url:
1244 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1245 access_token: token,
1247 domain: Pleroma.Web.Endpoint.host(),
1250 unfollow_modal: false,
1253 auto_play_gif: false,
1254 display_sensitive_media: false,
1255 reduce_motion: false,
1256 max_toot_chars: limit,
1257 mascot: "/images/pleroma-fox-tan-smol.png"
1260 delete_others_notice: present?(user.info.is_moderator),
1261 admin: present?(user.info.is_admin)
1265 default_privacy: user.info.default_scope,
1266 default_sensitive: false,
1267 allow_content_types: Config.get([:instance, :allowed_post_formats])
1269 media_attachments: %{
1270 accept_content_types: [
1286 user.info.settings ||
1316 push_subscription: nil,
1318 custom_emojis: mastodon_emoji,
1324 |> put_layout(false)
1325 |> put_view(MastodonView)
1326 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1329 |> put_session(:return_to, conn.request_path)
1330 |> redirect(to: "/web/login")
1334 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1335 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1337 with changeset <- Ecto.Changeset.change(user),
1338 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1339 {:ok, _user} <- User.update_and_set_cache(changeset) do
1344 |> put_resp_content_type("application/json")
1345 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1349 @supported_flavours ["glitch", "vanilla"]
1351 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1352 when flavour in @supported_flavours do
1353 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1355 with changeset <- Ecto.Changeset.change(user),
1356 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1357 {:ok, user} <- User.update_and_set_cache(changeset),
1358 flavour <- user.info.flavour do
1363 |> put_resp_content_type("application/json")
1364 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1368 def set_flavour(conn, _params) do
1371 |> json(%{error: "Unsupported flavour"})
1374 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1375 json(conn, get_user_flavour(user))
1378 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1382 defp get_user_flavour(_) do
1386 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1387 redirect(conn, to: local_mastodon_root_path(conn))
1390 @doc "Local Mastodon FE login init action"
1391 def login(conn, %{"code" => auth_token}) do
1392 with {:ok, app} <- get_or_make_app(),
1393 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1394 {:ok, token} <- Token.exchange_token(app, auth) do
1396 |> put_session(:oauth_token, token.token)
1397 |> redirect(to: local_mastodon_root_path(conn))
1401 @doc "Local Mastodon FE callback action"
1402 def login(conn, _) do
1403 with {:ok, app} <- get_or_make_app() do
1408 response_type: "code",
1409 client_id: app.client_id,
1411 scope: Enum.join(app.scopes, " ")
1414 redirect(conn, to: path)
1418 defp local_mastodon_root_path(conn) do
1419 case get_session(conn, :return_to) do
1421 mastodon_api_path(conn, :index, ["getting-started"])
1424 delete_session(conn, :return_to)
1429 defp get_or_make_app do
1430 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1431 scopes = ["read", "write", "follow", "push"]
1433 with %App{} = app <- Repo.get_by(App, find_attrs) do
1435 if app.scopes == scopes do
1439 |> Ecto.Changeset.change(%{scopes: scopes})
1447 App.register_changeset(
1449 Map.put(find_attrs, :scopes, scopes)
1456 def logout(conn, _) do
1459 |> redirect(to: "/")
1462 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1463 Logger.debug("Unimplemented, returning unmodified relationship")
1465 with %User{} = target <- User.get_by_id(id) do
1467 |> put_view(AccountView)
1468 |> render("relationship.json", %{user: user, target: target})
1472 def empty_array(conn, _) do
1473 Logger.debug("Unimplemented, returning an empty array")
1477 def empty_object(conn, _) do
1478 Logger.debug("Unimplemented, returning an empty object")
1482 def get_filters(%{assigns: %{user: user}} = conn, _) do
1483 filters = Filter.get_filters(user)
1484 res = FilterView.render("filters.json", filters: filters)
1489 %{assigns: %{user: user}} = conn,
1490 %{"phrase" => phrase, "context" => context} = params
1496 hide: Map.get(params, "irreversible", nil),
1497 whole_word: Map.get(params, "boolean", true)
1501 {:ok, response} = Filter.create(query)
1502 res = FilterView.render("filter.json", filter: response)
1506 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1507 filter = Filter.get(filter_id, user)
1508 res = FilterView.render("filter.json", filter: filter)
1513 %{assigns: %{user: user}} = conn,
1514 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1518 filter_id: filter_id,
1521 hide: Map.get(params, "irreversible", nil),
1522 whole_word: Map.get(params, "boolean", true)
1526 {:ok, response} = Filter.update(query)
1527 res = FilterView.render("filter.json", filter: response)
1531 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1534 filter_id: filter_id
1537 {:ok, _} = Filter.delete(query)
1543 def errors(conn, {:error, %Changeset{} = changeset}) do
1546 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1547 |> Enum.map_join(", ", fn {_k, v} -> v end)
1551 |> json(%{error: error_message})
1554 def errors(conn, {:error, :not_found}) do
1557 |> json(%{error: "Record not found"})
1560 def errors(conn, _) do
1563 |> json("Something went wrong")
1566 def suggestions(%{assigns: %{user: user}} = conn, _) do
1567 suggestions = Config.get(:suggestions)
1569 if Keyword.get(suggestions, :enabled, false) do
1570 api = Keyword.get(suggestions, :third_party_engine, "")
1571 timeout = Keyword.get(suggestions, :timeout, 5000)
1572 limit = Keyword.get(suggestions, :limit, 23)
1574 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1576 user = user.nickname
1580 |> String.replace("{{host}}", host)
1581 |> String.replace("{{user}}", user)
1583 with {:ok, %{status: 200, body: body}} <-
1588 recv_timeout: timeout,
1592 {:ok, data} <- Jason.decode(body) do
1595 |> Enum.slice(0, limit)
1600 case User.get_or_fetch(x["acct"]) do
1607 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1610 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1616 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1623 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1624 with %Activity{} = activity <- Activity.get_by_id(status_id),
1625 true <- Visibility.visible_for_user?(activity, user) do
1629 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1639 def reports(%{assigns: %{user: user}} = conn, params) do
1640 case CommonAPI.report(user, params) do
1643 |> put_view(ReportView)
1644 |> try_render("report.json", %{activity: activity})
1648 |> put_status(:bad_request)
1649 |> json(%{error: err})
1653 def try_render(conn, target, params)
1654 when is_binary(target) do
1655 res = render(conn, target, params)
1660 |> json(%{error: "Can't display this activity"})
1666 def try_render(conn, _, _) do
1669 |> json(%{error: "Can't display this activity"})
1672 defp present?(nil), do: false
1673 defp present?(false), do: false
1674 defp present?(_), do: true