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
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
14 alias Pleroma.Notification
16 alias Pleroma.Object.Fetcher
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
50 Pleroma.Plugs.RateLimitPlug,
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
55 when action in [:account_register]
58 @httpoison Application.get_env(:pleroma, :httpoison)
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(:errors)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
135 |> Map.put(:emoji, user_info_emojis)
137 info_cng = User.Info.profile_update(user.info, info_params)
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
151 |> json(%{error: "Invalid request"})
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 account = AccountView.render("account.json", %{user: user, for: user})
160 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
161 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
164 |> render("short.json", %{app: app})
168 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
169 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
170 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171 account = AccountView.render("account.json", %{user: user, for: for_user})
177 |> json(%{error: "Can't find user"})
181 @mastodon_api_level "2.7.2"
183 def masto_instance(conn, _params) do
184 instance = Config.get(:instance)
188 title: Keyword.get(instance, :name),
189 description: Keyword.get(instance, :description),
190 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
191 email: Keyword.get(instance, :email),
193 streaming_api: Pleroma.Web.Endpoint.websocket_url()
195 stats: Stats.get_stats(),
196 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
198 registrations: Pleroma.Config.get([:instance, :registrations_open]),
199 # Extra (not present in Mastodon):
200 max_toot_chars: Keyword.get(instance, :limit)
206 def peers(conn, _params) do
207 json(conn, Stats.get_peers())
210 defp mastodonized_emoji do
211 Pleroma.Emoji.get_all()
212 |> Enum.map(fn {shortcode, relative_url, tags} ->
213 url = to_string(URI.merge(Web.base_url(), relative_url))
216 "shortcode" => shortcode,
218 "visible_in_picker" => true,
225 def custom_emojis(conn, _params) do
226 mastodon_emoji = mastodonized_emoji()
227 json(conn, mastodon_emoji)
230 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
233 |> Map.drop(["since_id", "max_id", "min_id"])
236 last = List.last(activities)
243 |> Map.get("limit", "20")
244 |> String.to_integer()
247 if length(activities) <= limit do
253 |> Enum.at(limit * -1)
257 {next_url, prev_url} =
261 Pleroma.Web.Endpoint,
264 Map.merge(params, %{max_id: max_id})
267 Pleroma.Web.Endpoint,
270 Map.merge(params, %{min_id: min_id})
276 Pleroma.Web.Endpoint,
278 Map.merge(params, %{max_id: max_id})
281 Pleroma.Web.Endpoint,
283 Map.merge(params, %{min_id: min_id})
289 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
295 def home_timeline(%{assigns: %{user: user}} = conn, params) do
298 |> Map.put("type", ["Create", "Announce"])
299 |> Map.put("blocking_user", user)
300 |> Map.put("muting_user", user)
301 |> Map.put("user", user)
304 [user.ap_id | user.following]
305 |> ActivityPub.fetch_activities(params)
306 |> ActivityPub.contain_timeline(user)
310 |> add_link_headers(:home_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def public_timeline(%{assigns: %{user: user}} = conn, params) do
316 local_only = params["local"] in [true, "True", "true", "1"]
320 |> Map.put("type", ["Create", "Announce"])
321 |> Map.put("local_only", local_only)
322 |> Map.put("blocking_user", user)
323 |> Map.put("muting_user", user)
324 |> ActivityPub.fetch_public_activities()
328 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
329 |> put_view(StatusView)
330 |> render("index.json", %{activities: activities, for: user, as: :activity})
333 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
334 with %User{} = user <- User.get_cached_by_id(params["id"]) do
335 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
338 |> add_link_headers(:user_statuses, activities, params["id"])
339 |> put_view(StatusView)
340 |> render("index.json", %{
341 activities: activities,
348 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", "Create")
352 |> Map.put("blocking_user", user)
353 |> Map.put("user", user)
354 |> Map.put(:visibility, "direct")
358 |> ActivityPub.fetch_activities_query(params)
359 |> Pagination.fetch_paginated(params)
362 |> add_link_headers(:dm_timeline, activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
369 true <- Visibility.visible_for_user?(activity, user) do
371 |> put_view(StatusView)
372 |> try_render("status.json", %{activity: activity, for: user})
376 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id(id),
379 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 "blocking_user" => user,
384 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
386 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
387 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
393 activities: grouped_activities[true] || [],
397 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
402 activities: grouped_activities[false] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
413 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
414 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
416 |> add_link_headers(:scheduled_statuses, scheduled_activities)
417 |> put_view(ScheduledActivityView)
418 |> render("index.json", %{scheduled_activities: scheduled_activities})
422 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id) do
426 |> put_view(ScheduledActivityView)
427 |> render("show.json", %{scheduled_activity: scheduled_activity})
429 _ -> {:error, :not_found}
433 def update_scheduled_status(
434 %{assigns: %{user: user}} = conn,
435 %{"id" => scheduled_activity_id} = params
437 with %ScheduledActivity{} = scheduled_activity <-
438 ScheduledActivity.get(user, scheduled_activity_id),
439 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
441 |> put_view(ScheduledActivityView)
442 |> render("show.json", %{scheduled_activity: scheduled_activity})
444 nil -> {:error, :not_found}
449 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
450 with %ScheduledActivity{} = scheduled_activity <-
451 ScheduledActivity.get(user, scheduled_activity_id),
452 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
454 |> put_view(ScheduledActivityView)
455 |> render("show.json", %{scheduled_activity: scheduled_activity})
457 nil -> {:error, :not_found}
462 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
463 when length(media_ids) > 0 do
466 |> Map.put("status", ".")
468 post_status(conn, params)
471 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
474 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
477 case get_req_header(conn, "idempotency-key") do
479 _ -> Ecto.UUID.generate()
482 scheduled_at = params["scheduled_at"]
484 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
485 with {:ok, scheduled_activity} <-
486 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
488 |> put_view(ScheduledActivityView)
489 |> render("show.json", %{scheduled_activity: scheduled_activity})
492 params = Map.drop(params, ["scheduled_at"])
495 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
496 CommonAPI.post(user, params)
500 |> put_view(StatusView)
501 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
505 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
506 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
512 |> json(%{error: "Can't delete this post"})
516 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
517 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
518 %Activity{} = announce <- Activity.normalize(announce.data) do
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
525 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
527 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
529 |> put_view(StatusView)
530 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
534 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
535 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
536 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
538 |> put_view(StatusView)
539 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
543 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
544 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
545 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
552 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
553 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
555 |> put_view(StatusView)
556 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
560 |> put_resp_content_type("application/json")
561 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
565 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
566 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
568 |> put_view(StatusView)
569 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
573 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
574 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
575 %User{} = user <- User.get_cached_by_nickname(user.nickname),
576 true <- Visibility.visible_for_user?(activity, user),
577 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
579 |> put_view(StatusView)
580 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
584 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
585 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
586 %User{} = user <- User.get_cached_by_nickname(user.nickname),
587 true <- Visibility.visible_for_user?(activity, user),
588 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
590 |> put_view(StatusView)
591 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
596 activity = Activity.get_by_id(id)
598 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
600 |> put_view(StatusView)
601 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
605 |> put_resp_content_type("application/json")
606 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
610 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
611 activity = Activity.get_by_id(id)
613 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
615 |> put_view(StatusView)
616 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
620 def notifications(%{assigns: %{user: user}} = conn, params) do
621 notifications = MastodonAPI.get_notifications(user, params)
624 |> add_link_headers(:notifications, notifications)
625 |> put_view(NotificationView)
626 |> render("index.json", %{notifications: notifications, for: user})
629 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
630 with {:ok, notification} <- Notification.get(user, id) do
632 |> put_view(NotificationView)
633 |> render("show.json", %{notification: notification, for: user})
637 |> put_resp_content_type("application/json")
638 |> send_resp(403, Jason.encode!(%{"error" => reason}))
642 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
643 Notification.clear(user)
647 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
648 with {:ok, _notif} <- Notification.dismiss(user, id) do
653 |> put_resp_content_type("application/json")
654 |> send_resp(403, Jason.encode!(%{"error" => reason}))
658 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
659 Notification.destroy_multiple(user, ids)
663 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
665 q = from(u in User, where: u.id in ^id)
666 targets = Repo.all(q)
669 |> put_view(AccountView)
670 |> render("relationships.json", %{user: user, targets: targets})
673 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
674 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
676 def update_media(%{assigns: %{user: user}} = conn, data) do
677 with %Object{} = object <- Repo.get(Object, data["id"]),
678 true <- Object.authorize_mutation(object, user),
679 true <- is_binary(data["description"]),
680 description <- data["description"] do
681 new_data = %{object.data | "name" => description}
685 |> Object.change(%{data: new_data})
688 attachment_data = Map.put(new_data, "id", object.id)
691 |> put_view(StatusView)
692 |> render("attachment.json", %{attachment: attachment_data})
696 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
697 with {:ok, object} <-
700 actor: User.ap_id(user),
701 description: Map.get(data, "description")
703 attachment_data = Map.put(object.data, "id", object.id)
706 |> put_view(StatusView)
707 |> render("attachment.json", %{attachment: attachment_data})
711 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
712 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
713 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
714 q = from(u in User, where: u.ap_id in ^likes)
718 |> put_view(AccountView)
719 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
725 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
726 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
727 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
728 q = from(u in User, where: u.ap_id in ^announces)
732 |> put_view(AccountView)
733 |> render("accounts.json", %{for: user, users: users, as: :user})
739 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
740 local_only = params["local"] in [true, "True", "true", "1"]
743 [params["tag"], params["any"]]
747 |> Enum.map(&String.downcase(&1))
752 |> Enum.map(&String.downcase(&1))
757 |> Enum.map(&String.downcase(&1))
761 |> Map.put("type", "Create")
762 |> Map.put("local_only", local_only)
763 |> Map.put("blocking_user", user)
764 |> Map.put("muting_user", user)
765 |> Map.put("tag", tags)
766 |> Map.put("tag_all", tag_all)
767 |> Map.put("tag_reject", tag_reject)
768 |> ActivityPub.fetch_public_activities()
772 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
773 |> put_view(StatusView)
774 |> render("index.json", %{activities: activities, for: user, as: :activity})
777 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
778 with %User{} = user <- User.get_cached_by_id(id),
779 followers <- MastodonAPI.get_followers(user, params) do
782 for_user && user.id == for_user.id -> followers
783 user.info.hide_followers -> []
788 |> add_link_headers(:followers, followers, user)
789 |> put_view(AccountView)
790 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
794 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
795 with %User{} = user <- User.get_cached_by_id(id),
796 followers <- MastodonAPI.get_friends(user, params) do
799 for_user && user.id == for_user.id -> followers
800 user.info.hide_follows -> []
805 |> add_link_headers(:following, followers, user)
806 |> put_view(AccountView)
807 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
811 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
812 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
814 |> put_view(AccountView)
815 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
819 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
820 with %User{} = follower <- User.get_cached_by_id(id),
821 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
823 |> put_view(AccountView)
824 |> render("relationship.json", %{user: followed, target: follower})
828 |> put_resp_content_type("application/json")
829 |> send_resp(403, Jason.encode!(%{"error" => message}))
833 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
834 with %User{} = follower <- User.get_cached_by_id(id),
835 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
837 |> put_view(AccountView)
838 |> render("relationship.json", %{user: followed, target: follower})
842 |> put_resp_content_type("application/json")
843 |> send_resp(403, Jason.encode!(%{"error" => message}))
847 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
848 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
849 {_, true} <- {:followed, follower.id != followed.id},
850 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
852 |> put_view(AccountView)
853 |> render("relationship.json", %{user: follower, target: followed})
860 |> put_resp_content_type("application/json")
861 |> send_resp(403, Jason.encode!(%{"error" => message}))
865 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
866 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
867 {_, true} <- {:followed, follower.id != followed.id},
868 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
870 |> put_view(AccountView)
871 |> render("account.json", %{user: followed, for: follower})
878 |> put_resp_content_type("application/json")
879 |> send_resp(403, Jason.encode!(%{"error" => message}))
883 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
884 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
885 {_, true} <- {:followed, follower.id != followed.id},
886 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
888 |> put_view(AccountView)
889 |> render("relationship.json", %{user: follower, target: followed})
899 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
900 with %User{} = muted <- User.get_cached_by_id(id),
901 {:ok, muter} <- User.mute(muter, muted) do
903 |> put_view(AccountView)
904 |> render("relationship.json", %{user: muter, target: muted})
908 |> put_resp_content_type("application/json")
909 |> send_resp(403, Jason.encode!(%{"error" => message}))
913 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
914 with %User{} = muted <- User.get_cached_by_id(id),
915 {:ok, muter} <- User.unmute(muter, muted) do
917 |> put_view(AccountView)
918 |> render("relationship.json", %{user: muter, target: muted})
922 |> put_resp_content_type("application/json")
923 |> send_resp(403, Jason.encode!(%{"error" => message}))
927 def mutes(%{assigns: %{user: user}} = conn, _) do
928 with muted_accounts <- User.muted_users(user) do
929 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
934 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
935 with %User{} = blocked <- User.get_cached_by_id(id),
936 {:ok, blocker} <- User.block(blocker, blocked),
937 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
939 |> put_view(AccountView)
940 |> render("relationship.json", %{user: blocker, target: blocked})
944 |> put_resp_content_type("application/json")
945 |> send_resp(403, Jason.encode!(%{"error" => message}))
949 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
950 with %User{} = blocked <- User.get_cached_by_id(id),
951 {:ok, blocker} <- User.unblock(blocker, blocked),
952 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: blocker, target: blocked})
959 |> put_resp_content_type("application/json")
960 |> send_resp(403, Jason.encode!(%{"error" => message}))
964 def blocks(%{assigns: %{user: user}} = conn, _) do
965 with blocked_accounts <- User.blocked_users(user) do
966 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
971 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
972 json(conn, info.domain_blocks || [])
975 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
976 User.block_domain(blocker, domain)
980 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
981 User.unblock_domain(blocker, domain)
985 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %User{} = subscription_target <- User.get_cached_by_id(id),
987 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: user, target: subscription_target})
994 |> put_resp_content_type("application/json")
995 |> send_resp(403, Jason.encode!(%{"error" => message}))
999 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1000 with %User{} = subscription_target <- User.get_cached_by_id(id),
1001 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1003 |> put_view(AccountView)
1004 |> render("relationship.json", %{user: user, target: subscription_target})
1006 {:error, message} ->
1008 |> put_resp_content_type("application/json")
1009 |> send_resp(403, Jason.encode!(%{"error" => message}))
1013 def status_search(user, query) do
1015 if Regex.match?(~r/https?:/, query) do
1016 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1017 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1018 true <- Visibility.visible_for_user?(activity, user) do
1027 [a, o] in Activity.with_preloaded_object(Activity),
1028 where: fragment("?->>'type' = 'Create'", a.data),
1029 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1032 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1037 order_by: [desc: :id]
1040 Repo.all(q) ++ fetched
1043 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1044 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1046 statuses = status_search(user, query)
1048 tags_path = Web.base_url() <> "/tag/"
1054 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1055 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1056 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1059 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1061 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1068 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1069 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1071 statuses = status_search(user, query)
1077 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1078 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1081 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1083 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1090 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1091 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1093 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1098 def favourites(%{assigns: %{user: user}} = conn, params) do
1101 |> Map.put("type", "Create")
1102 |> Map.put("favorited_by", user.ap_id)
1103 |> Map.put("blocking_user", user)
1106 ActivityPub.fetch_activities([], params)
1110 |> add_link_headers(:favourites, activities)
1111 |> put_view(StatusView)
1112 |> render("index.json", %{activities: activities, for: user, as: :activity})
1115 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1116 with %User{} = user <- User.get_by_id(id),
1117 false <- user.info.hide_favorites do
1120 |> Map.put("type", "Create")
1121 |> Map.put("favorited_by", user.ap_id)
1122 |> Map.put("blocking_user", for_user)
1126 ["https://www.w3.org/ns/activitystreams#Public"] ++
1127 [for_user.ap_id | for_user.following]
1129 ["https://www.w3.org/ns/activitystreams#Public"]
1134 |> ActivityPub.fetch_activities(params)
1138 |> add_link_headers(:favourites, activities)
1139 |> put_view(StatusView)
1140 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1143 {:error, :not_found}
1148 |> json(%{error: "Can't get favorites"})
1152 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1153 user = User.get_cached_by_id(user.id)
1156 Bookmark.for_user_query(user.id)
1157 |> Pagination.fetch_paginated(params)
1161 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1164 |> add_link_headers(:bookmarks, bookmarks)
1165 |> put_view(StatusView)
1166 |> render("index.json", %{activities: activities, for: user, as: :activity})
1169 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1170 lists = Pleroma.List.for_user(user, opts)
1171 res = ListView.render("lists.json", lists: lists)
1175 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1176 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1177 res = ListView.render("list.json", list: list)
1183 |> json(%{error: "Record not found"})
1187 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1188 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1189 res = ListView.render("lists.json", lists: lists)
1193 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1194 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1195 {:ok, _list} <- Pleroma.List.delete(list) do
1203 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1204 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1205 res = ListView.render("list.json", list: list)
1210 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1212 |> Enum.each(fn account_id ->
1213 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1214 %User{} = followed <- User.get_cached_by_id(account_id) do
1215 Pleroma.List.follow(list, followed)
1222 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1224 |> Enum.each(fn account_id ->
1225 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1226 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1227 Pleroma.List.unfollow(list, followed)
1234 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1235 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1236 {:ok, users} = Pleroma.List.get_following(list) do
1238 |> put_view(AccountView)
1239 |> render("accounts.json", %{for: user, users: users, as: :user})
1243 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1244 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1245 {:ok, list} <- Pleroma.List.rename(list, title) do
1246 res = ListView.render("list.json", list: list)
1254 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1255 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1258 |> Map.put("type", "Create")
1259 |> Map.put("blocking_user", user)
1260 |> Map.put("muting_user", user)
1262 # we must filter the following list for the user to avoid leaking statuses the user
1263 # does not actually have permission to see (for more info, peruse security issue #270).
1266 |> Enum.filter(fn x -> x in user.following end)
1267 |> ActivityPub.fetch_activities_bounded(following, params)
1271 |> put_view(StatusView)
1272 |> render("index.json", %{activities: activities, for: user, as: :activity})
1277 |> json(%{error: "Error."})
1281 def index(%{assigns: %{user: user}} = conn, _params) do
1282 token = get_session(conn, :oauth_token)
1285 mastodon_emoji = mastodonized_emoji()
1287 limit = Config.get([:instance, :limit])
1290 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1292 flavour = get_user_flavour(user)
1297 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1298 access_token: token,
1300 domain: Pleroma.Web.Endpoint.host(),
1303 unfollow_modal: false,
1306 auto_play_gif: false,
1307 display_sensitive_media: false,
1308 reduce_motion: false,
1309 max_toot_chars: limit,
1310 mascot: "/images/pleroma-fox-tan-smol.png"
1313 delete_others_notice: present?(user.info.is_moderator),
1314 admin: present?(user.info.is_admin)
1318 default_privacy: user.info.default_scope,
1319 default_sensitive: false,
1320 allow_content_types: Config.get([:instance, :allowed_post_formats])
1322 media_attachments: %{
1323 accept_content_types: [
1339 user.info.settings ||
1369 push_subscription: nil,
1371 custom_emojis: mastodon_emoji,
1377 |> put_layout(false)
1378 |> put_view(MastodonView)
1379 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1382 |> put_session(:return_to, conn.request_path)
1383 |> redirect(to: "/web/login")
1387 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1388 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1390 with changeset <- Ecto.Changeset.change(user),
1391 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1392 {:ok, _user} <- User.update_and_set_cache(changeset) do
1397 |> put_resp_content_type("application/json")
1398 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1402 @supported_flavours ["glitch", "vanilla"]
1404 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1405 when flavour in @supported_flavours do
1406 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1408 with changeset <- Ecto.Changeset.change(user),
1409 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1410 {:ok, user} <- User.update_and_set_cache(changeset),
1411 flavour <- user.info.flavour do
1416 |> put_resp_content_type("application/json")
1417 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1421 def set_flavour(conn, _params) do
1424 |> json(%{error: "Unsupported flavour"})
1427 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1428 json(conn, get_user_flavour(user))
1431 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1435 defp get_user_flavour(_) do
1439 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1440 redirect(conn, to: local_mastodon_root_path(conn))
1443 @doc "Local Mastodon FE login init action"
1444 def login(conn, %{"code" => auth_token}) do
1445 with {:ok, app} <- get_or_make_app(),
1446 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1447 {:ok, token} <- Token.exchange_token(app, auth) do
1449 |> put_session(:oauth_token, token.token)
1450 |> redirect(to: local_mastodon_root_path(conn))
1454 @doc "Local Mastodon FE callback action"
1455 def login(conn, _) do
1456 with {:ok, app} <- get_or_make_app() do
1461 response_type: "code",
1462 client_id: app.client_id,
1464 scope: Enum.join(app.scopes, " ")
1467 redirect(conn, to: path)
1471 defp local_mastodon_root_path(conn) do
1472 case get_session(conn, :return_to) do
1474 mastodon_api_path(conn, :index, ["getting-started"])
1477 delete_session(conn, :return_to)
1482 defp get_or_make_app do
1483 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1484 scopes = ["read", "write", "follow", "push"]
1486 with %App{} = app <- Repo.get_by(App, find_attrs) do
1488 if app.scopes == scopes do
1492 |> Ecto.Changeset.change(%{scopes: scopes})
1500 App.register_changeset(
1502 Map.put(find_attrs, :scopes, scopes)
1509 def logout(conn, _) do
1512 |> redirect(to: "/")
1515 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1516 Logger.debug("Unimplemented, returning unmodified relationship")
1518 with %User{} = target <- User.get_cached_by_id(id) do
1520 |> put_view(AccountView)
1521 |> render("relationship.json", %{user: user, target: target})
1525 def empty_array(conn, _) do
1526 Logger.debug("Unimplemented, returning an empty array")
1530 def empty_object(conn, _) do
1531 Logger.debug("Unimplemented, returning an empty object")
1535 def get_filters(%{assigns: %{user: user}} = conn, _) do
1536 filters = Filter.get_filters(user)
1537 res = FilterView.render("filters.json", filters: filters)
1542 %{assigns: %{user: user}} = conn,
1543 %{"phrase" => phrase, "context" => context} = params
1549 hide: Map.get(params, "irreversible", false),
1550 whole_word: Map.get(params, "boolean", true)
1554 {:ok, response} = Filter.create(query)
1555 res = FilterView.render("filter.json", filter: response)
1559 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1560 filter = Filter.get(filter_id, user)
1561 res = FilterView.render("filter.json", filter: filter)
1566 %{assigns: %{user: user}} = conn,
1567 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1571 filter_id: filter_id,
1574 hide: Map.get(params, "irreversible", nil),
1575 whole_word: Map.get(params, "boolean", true)
1579 {:ok, response} = Filter.update(query)
1580 res = FilterView.render("filter.json", filter: response)
1584 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1587 filter_id: filter_id
1590 {:ok, _} = Filter.delete(query)
1596 def errors(conn, {:error, %Changeset{} = changeset}) do
1599 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1600 |> Enum.map_join(", ", fn {_k, v} -> v end)
1604 |> json(%{error: error_message})
1607 def errors(conn, {:error, :not_found}) do
1610 |> json(%{error: "Record not found"})
1613 def errors(conn, _) do
1616 |> json("Something went wrong")
1619 def suggestions(%{assigns: %{user: user}} = conn, _) do
1620 suggestions = Config.get(:suggestions)
1622 if Keyword.get(suggestions, :enabled, false) do
1623 api = Keyword.get(suggestions, :third_party_engine, "")
1624 timeout = Keyword.get(suggestions, :timeout, 5000)
1625 limit = Keyword.get(suggestions, :limit, 23)
1627 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1629 user = user.nickname
1633 |> String.replace("{{host}}", host)
1634 |> String.replace("{{user}}", user)
1636 with {:ok, %{status: 200, body: body}} <-
1641 recv_timeout: timeout,
1645 {:ok, data} <- Jason.decode(body) do
1648 |> Enum.slice(0, limit)
1653 case User.get_or_fetch(x["acct"]) do
1654 {:ok, %User{id: id}} -> id
1660 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1663 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1669 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1676 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1677 with %Activity{} = activity <- Activity.get_by_id(status_id),
1678 true <- Visibility.visible_for_user?(activity, user) do
1682 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1692 def reports(%{assigns: %{user: user}} = conn, params) do
1693 case CommonAPI.report(user, params) do
1696 |> put_view(ReportView)
1697 |> try_render("report.json", %{activity: activity})
1701 |> put_status(:bad_request)
1702 |> json(%{error: err})
1706 def account_register(
1707 %{assigns: %{app: app}} = conn,
1708 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1716 "captcha_answer_data",
1720 |> Map.put("nickname", nickname)
1721 |> Map.put("fullname", params["fullname"] || nickname)
1722 |> Map.put("bio", params["bio"] || "")
1723 |> Map.put("confirm", params["password"])
1725 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1726 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1728 token_type: "Bearer",
1729 access_token: token.token,
1731 created_at: Token.Utils.format_created_at(token)
1737 |> json(Jason.encode!(errors))
1741 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1744 |> json(%{error: "Missing parameters"})
1747 def account_register(conn, _) do
1750 |> json(%{error: "Invalid credentials"})
1753 def conversations(%{assigns: %{user: user}} = conn, params) do
1754 participations = Participation.for_user_with_last_activity_id(user, params)
1757 Enum.map(participations, fn participation ->
1758 ConversationView.render("participation.json", %{participation: participation, user: user})
1762 |> add_link_headers(:conversations, participations)
1763 |> json(conversations)
1766 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1767 with %Participation{} = participation <-
1768 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1769 {:ok, participation} <- Participation.mark_as_read(participation) do
1770 participation_view =
1771 ConversationView.render("participation.json", %{participation: participation, user: user})
1774 |> json(participation_view)
1778 def try_render(conn, target, params)
1779 when is_binary(target) do
1780 res = render(conn, target, params)
1785 |> json(%{error: "Can't display this activity"})
1791 def try_render(conn, _, _) do
1794 |> json(%{error: "Can't display this activity"})
1797 defp present?(nil), do: false
1798 defp present?(false), do: false
1799 defp present?(_), do: true