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
15 alias Pleroma.Notification
17 alias Pleroma.Object.Fetcher
18 alias Pleroma.Pagination
20 alias Pleroma.ScheduledActivity
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ConversationView
30 alias Pleroma.Web.MastodonAPI.FilterView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.NotificationView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
45 alias Pleroma.Web.ControllerHelper
51 Pleroma.Plugs.RateLimitPlug,
53 max_requests: Config.get([:app_account_creation, :max_requests]),
54 interval: Config.get([:app_account_creation, :interval])
56 when action in [:account_register]
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))
127 :skip_thread_containment
129 |> Enum.reduce(%{}, fn key, acc ->
130 add_if_present(acc, params, to_string(key), key, fn value ->
131 {:ok, ControllerHelper.truthy_param?(value)}
134 |> add_if_present(params, "default_scope", :default_scope)
135 |> add_if_present(params, "header", :banner, fn value ->
136 with %Plug.Upload{} <- value,
137 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
143 |> Map.put(:emoji, user_info_emojis)
145 info_cng = User.Info.profile_update(user.info, info_params)
147 with changeset <- User.update_changeset(user, user_params),
148 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
149 {:ok, user} <- User.update_and_set_cache(changeset) do
150 if original_user != user do
151 CommonAPI.update(user)
154 json(conn, AccountView.render("account.json", %{user: user, for: user}))
159 |> json(%{error: "Invalid request"})
163 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
164 account = AccountView.render("account.json", %{user: user, for: user})
168 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
169 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
172 |> render("short.json", %{app: app})
176 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
177 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
178 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
179 account = AccountView.render("account.json", %{user: user, for: for_user})
185 |> json(%{error: "Can't find user"})
189 @mastodon_api_level "2.7.2"
191 def masto_instance(conn, _params) do
192 instance = Config.get(:instance)
196 title: Keyword.get(instance, :name),
197 description: Keyword.get(instance, :description),
198 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
199 email: Keyword.get(instance, :email),
201 streaming_api: Pleroma.Web.Endpoint.websocket_url()
203 stats: Stats.get_stats(),
204 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
206 registrations: Pleroma.Config.get([:instance, :registrations_open]),
207 # Extra (not present in Mastodon):
208 max_toot_chars: Keyword.get(instance, :limit)
214 def peers(conn, _params) do
215 json(conn, Stats.get_peers())
218 defp mastodonized_emoji do
219 Pleroma.Emoji.get_all()
220 |> Enum.map(fn {shortcode, relative_url, tags} ->
221 url = to_string(URI.merge(Web.base_url(), relative_url))
224 "shortcode" => shortcode,
226 "visible_in_picker" => true,
233 def custom_emojis(conn, _params) do
234 mastodon_emoji = mastodonized_emoji()
235 json(conn, mastodon_emoji)
238 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
241 |> Map.drop(["since_id", "max_id", "min_id"])
244 last = List.last(activities)
251 |> Map.get("limit", "20")
252 |> String.to_integer()
255 if length(activities) <= limit do
261 |> Enum.at(limit * -1)
265 {next_url, prev_url} =
269 Pleroma.Web.Endpoint,
272 Map.merge(params, %{max_id: max_id})
275 Pleroma.Web.Endpoint,
278 Map.merge(params, %{min_id: min_id})
284 Pleroma.Web.Endpoint,
286 Map.merge(params, %{max_id: max_id})
289 Pleroma.Web.Endpoint,
291 Map.merge(params, %{min_id: min_id})
297 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
303 def home_timeline(%{assigns: %{user: user}} = conn, params) do
306 |> Map.put("type", ["Create", "Announce"])
307 |> Map.put("blocking_user", user)
308 |> Map.put("muting_user", user)
309 |> Map.put("user", user)
312 [user.ap_id | user.following]
313 |> ActivityPub.fetch_activities(params)
317 |> add_link_headers(:home_timeline, activities)
318 |> put_view(StatusView)
319 |> render("index.json", %{activities: activities, for: user, as: :activity})
322 def public_timeline(%{assigns: %{user: user}} = conn, params) do
323 local_only = params["local"] in [true, "True", "true", "1"]
327 |> Map.put("type", ["Create", "Announce"])
328 |> Map.put("local_only", local_only)
329 |> Map.put("blocking_user", user)
330 |> Map.put("muting_user", user)
331 |> ActivityPub.fetch_public_activities()
335 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
336 |> put_view(StatusView)
337 |> render("index.json", %{activities: activities, for: user, as: :activity})
340 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
341 with %User{} = user <- User.get_cached_by_id(params["id"]) do
342 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
345 |> add_link_headers(:user_statuses, activities, params["id"])
346 |> put_view(StatusView)
347 |> render("index.json", %{
348 activities: activities,
355 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
358 |> Map.put("type", "Create")
359 |> Map.put("blocking_user", user)
360 |> Map.put("user", user)
361 |> Map.put(:visibility, "direct")
365 |> ActivityPub.fetch_activities_query(params)
366 |> Pagination.fetch_paginated(params)
369 |> add_link_headers(:dm_timeline, activities)
370 |> put_view(StatusView)
371 |> render("index.json", %{activities: activities, for: user, as: :activity})
374 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
375 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
376 true <- Visibility.visible_for_user?(activity, user) do
378 |> put_view(StatusView)
379 |> try_render("status.json", %{activity: activity, for: user})
383 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
384 with %Activity{} = activity <- Activity.get_by_id(id),
386 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
387 "blocking_user" => user,
391 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
393 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
394 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
400 activities: grouped_activities[true] || [],
404 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
409 activities: grouped_activities[false] || [],
413 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
420 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
421 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
423 |> add_link_headers(:scheduled_statuses, scheduled_activities)
424 |> put_view(ScheduledActivityView)
425 |> render("index.json", %{scheduled_activities: scheduled_activities})
429 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
430 with %ScheduledActivity{} = scheduled_activity <-
431 ScheduledActivity.get(user, scheduled_activity_id) do
433 |> put_view(ScheduledActivityView)
434 |> render("show.json", %{scheduled_activity: scheduled_activity})
436 _ -> {:error, :not_found}
440 def update_scheduled_status(
441 %{assigns: %{user: user}} = conn,
442 %{"id" => scheduled_activity_id} = params
444 with %ScheduledActivity{} = scheduled_activity <-
445 ScheduledActivity.get(user, scheduled_activity_id),
446 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
448 |> put_view(ScheduledActivityView)
449 |> render("show.json", %{scheduled_activity: scheduled_activity})
451 nil -> {:error, :not_found}
456 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
457 with %ScheduledActivity{} = scheduled_activity <-
458 ScheduledActivity.get(user, scheduled_activity_id),
459 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
464 nil -> {:error, :not_found}
469 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
470 when length(media_ids) > 0 do
473 |> Map.put("status", ".")
475 post_status(conn, params)
478 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
481 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
484 case get_req_header(conn, "idempotency-key") do
486 _ -> Ecto.UUID.generate()
489 scheduled_at = params["scheduled_at"]
491 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
492 with {:ok, scheduled_activity} <-
493 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
495 |> put_view(ScheduledActivityView)
496 |> render("show.json", %{scheduled_activity: scheduled_activity})
499 params = Map.drop(params, ["scheduled_at"])
502 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
503 CommonAPI.post(user, params)
507 |> put_view(StatusView)
508 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
512 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
513 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
519 |> json(%{error: "Can't delete this post"})
523 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
524 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
525 %Activity{} = announce <- Activity.normalize(announce.data) do
527 |> put_view(StatusView)
528 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
532 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
533 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
534 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
536 |> put_view(StatusView)
537 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
541 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
542 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
543 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
545 |> put_view(StatusView)
546 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
550 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
551 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
552 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
554 |> put_view(StatusView)
555 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
559 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
560 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
567 |> put_resp_content_type("application/json")
568 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
572 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
573 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
575 |> put_view(StatusView)
576 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
580 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
581 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
582 %User{} = user <- User.get_cached_by_nickname(user.nickname),
583 true <- Visibility.visible_for_user?(activity, user),
584 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
586 |> put_view(StatusView)
587 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
591 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
592 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
593 %User{} = user <- User.get_cached_by_nickname(user.nickname),
594 true <- Visibility.visible_for_user?(activity, user),
595 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
597 |> put_view(StatusView)
598 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
602 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
603 activity = Activity.get_by_id(id)
605 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
607 |> put_view(StatusView)
608 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
612 |> put_resp_content_type("application/json")
613 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
617 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 activity = Activity.get_by_id(id)
620 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
627 def notifications(%{assigns: %{user: user}} = conn, params) do
628 notifications = MastodonAPI.get_notifications(user, params)
631 |> add_link_headers(:notifications, notifications)
632 |> put_view(NotificationView)
633 |> render("index.json", %{notifications: notifications, for: user})
636 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
637 with {:ok, notification} <- Notification.get(user, id) do
639 |> put_view(NotificationView)
640 |> render("show.json", %{notification: notification, for: user})
644 |> put_resp_content_type("application/json")
645 |> send_resp(403, Jason.encode!(%{"error" => reason}))
649 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
650 Notification.clear(user)
654 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
655 with {:ok, _notif} <- Notification.dismiss(user, id) do
660 |> put_resp_content_type("application/json")
661 |> send_resp(403, Jason.encode!(%{"error" => reason}))
665 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
666 Notification.destroy_multiple(user, ids)
670 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
672 q = from(u in User, where: u.id in ^id)
673 targets = Repo.all(q)
676 |> put_view(AccountView)
677 |> render("relationships.json", %{user: user, targets: targets})
680 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
681 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
683 def update_media(%{assigns: %{user: user}} = conn, data) do
684 with %Object{} = object <- Repo.get(Object, data["id"]),
685 true <- Object.authorize_mutation(object, user),
686 true <- is_binary(data["description"]),
687 description <- data["description"] do
688 new_data = %{object.data | "name" => description}
692 |> Object.change(%{data: new_data})
695 attachment_data = Map.put(new_data, "id", object.id)
698 |> put_view(StatusView)
699 |> render("attachment.json", %{attachment: attachment_data})
703 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
704 with {:ok, object} <-
707 actor: User.ap_id(user),
708 description: Map.get(data, "description")
710 attachment_data = Map.put(object.data, "id", object.id)
713 |> put_view(StatusView)
714 |> render("attachment.json", %{attachment: attachment_data})
718 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
719 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
720 %{} = attachment_data <- Map.put(object.data, "id", object.id),
721 %{type: type} = rendered <-
722 StatusView.render("attachment.json", %{attachment: attachment_data}) do
723 # Reject if not an image
724 if type == "image" do
726 # Save to the user's info
727 info_changeset = User.Info.mascot_update(user.info, rendered)
731 |> Ecto.Changeset.change()
732 |> Ecto.Changeset.put_embed(:info, info_changeset)
734 {:ok, _user} = User.update_and_set_cache(user_changeset)
740 |> put_resp_content_type("application/json")
741 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
746 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
747 mascot = User.get_mascot(user)
753 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
754 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
755 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
756 q = from(u in User, where: u.ap_id in ^likes)
760 |> put_view(AccountView)
761 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
767 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
768 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
769 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
770 q = from(u in User, where: u.ap_id in ^announces)
774 |> put_view(AccountView)
775 |> render("accounts.json", %{for: user, users: users, as: :user})
781 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
782 local_only = params["local"] in [true, "True", "true", "1"]
785 [params["tag"], params["any"]]
789 |> Enum.map(&String.downcase(&1))
794 |> Enum.map(&String.downcase(&1))
799 |> Enum.map(&String.downcase(&1))
803 |> Map.put("type", "Create")
804 |> Map.put("local_only", local_only)
805 |> Map.put("blocking_user", user)
806 |> Map.put("muting_user", user)
807 |> Map.put("tag", tags)
808 |> Map.put("tag_all", tag_all)
809 |> Map.put("tag_reject", tag_reject)
810 |> ActivityPub.fetch_public_activities()
814 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
815 |> put_view(StatusView)
816 |> render("index.json", %{activities: activities, for: user, as: :activity})
819 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
820 with %User{} = user <- User.get_cached_by_id(id),
821 followers <- MastodonAPI.get_followers(user, params) do
824 for_user && user.id == for_user.id -> followers
825 user.info.hide_followers -> []
830 |> add_link_headers(:followers, followers, user)
831 |> put_view(AccountView)
832 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
836 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
837 with %User{} = user <- User.get_cached_by_id(id),
838 followers <- MastodonAPI.get_friends(user, params) do
841 for_user && user.id == for_user.id -> followers
842 user.info.hide_follows -> []
847 |> add_link_headers(:following, followers, user)
848 |> put_view(AccountView)
849 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
853 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
854 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
856 |> put_view(AccountView)
857 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
861 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
862 with %User{} = follower <- User.get_cached_by_id(id),
863 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
865 |> put_view(AccountView)
866 |> render("relationship.json", %{user: followed, target: follower})
870 |> put_resp_content_type("application/json")
871 |> send_resp(403, Jason.encode!(%{"error" => message}))
875 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
876 with %User{} = follower <- User.get_cached_by_id(id),
877 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
879 |> put_view(AccountView)
880 |> render("relationship.json", %{user: followed, target: follower})
884 |> put_resp_content_type("application/json")
885 |> send_resp(403, Jason.encode!(%{"error" => message}))
889 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
890 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
891 {_, true} <- {:followed, follower.id != followed.id},
892 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
894 |> put_view(AccountView)
895 |> render("relationship.json", %{user: follower, target: followed})
902 |> put_resp_content_type("application/json")
903 |> send_resp(403, Jason.encode!(%{"error" => message}))
907 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
908 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
909 {_, true} <- {:followed, follower.id != followed.id},
910 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
912 |> put_view(AccountView)
913 |> render("account.json", %{user: followed, for: follower})
920 |> put_resp_content_type("application/json")
921 |> send_resp(403, Jason.encode!(%{"error" => message}))
925 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
926 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
927 {_, true} <- {:followed, follower.id != followed.id},
928 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
930 |> put_view(AccountView)
931 |> render("relationship.json", %{user: follower, target: followed})
941 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
942 with %User{} = muted <- User.get_cached_by_id(id),
943 {:ok, muter} <- User.mute(muter, muted) do
945 |> put_view(AccountView)
946 |> render("relationship.json", %{user: muter, target: muted})
950 |> put_resp_content_type("application/json")
951 |> send_resp(403, Jason.encode!(%{"error" => message}))
955 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
956 with %User{} = muted <- User.get_cached_by_id(id),
957 {:ok, muter} <- User.unmute(muter, muted) do
959 |> put_view(AccountView)
960 |> render("relationship.json", %{user: muter, target: muted})
964 |> put_resp_content_type("application/json")
965 |> send_resp(403, Jason.encode!(%{"error" => message}))
969 def mutes(%{assigns: %{user: user}} = conn, _) do
970 with muted_accounts <- User.muted_users(user) do
971 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
976 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
977 with %User{} = blocked <- User.get_cached_by_id(id),
978 {:ok, blocker} <- User.block(blocker, blocked),
979 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
981 |> put_view(AccountView)
982 |> render("relationship.json", %{user: blocker, target: blocked})
986 |> put_resp_content_type("application/json")
987 |> send_resp(403, Jason.encode!(%{"error" => message}))
991 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
992 with %User{} = blocked <- User.get_cached_by_id(id),
993 {:ok, blocker} <- User.unblock(blocker, blocked),
994 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: blocker, target: blocked})
1001 |> put_resp_content_type("application/json")
1002 |> send_resp(403, Jason.encode!(%{"error" => message}))
1006 def blocks(%{assigns: %{user: user}} = conn, _) do
1007 with blocked_accounts <- User.blocked_users(user) do
1008 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1013 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1014 json(conn, info.domain_blocks || [])
1017 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1018 User.block_domain(blocker, domain)
1022 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1023 User.unblock_domain(blocker, domain)
1027 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1028 with %User{} = subscription_target <- User.get_cached_by_id(id),
1029 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1031 |> put_view(AccountView)
1032 |> render("relationship.json", %{user: user, target: subscription_target})
1034 {:error, message} ->
1036 |> put_resp_content_type("application/json")
1037 |> send_resp(403, Jason.encode!(%{"error" => message}))
1041 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1042 with %User{} = subscription_target <- User.get_cached_by_id(id),
1043 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1045 |> put_view(AccountView)
1046 |> render("relationship.json", %{user: user, target: subscription_target})
1048 {:error, message} ->
1050 |> put_resp_content_type("application/json")
1051 |> send_resp(403, Jason.encode!(%{"error" => message}))
1055 def status_search_query_with_gin(q, query) do
1059 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1063 order_by: [desc: :id]
1067 def status_search_query_with_rum(q, query) do
1071 "? @@ plainto_tsquery('english', ?)",
1075 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1079 def status_search(user, query) do
1081 if Regex.match?(~r/https?:/, query) do
1082 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1083 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1084 true <- Visibility.visible_for_user?(activity, user) do
1092 from([a, o] in Activity.with_preloaded_object(Activity),
1093 where: fragment("?->>'type' = 'Create'", a.data),
1094 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1099 if Pleroma.Config.get([:database, :rum_enabled]) do
1100 status_search_query_with_rum(q, query)
1102 status_search_query_with_gin(q, query)
1105 Repo.all(q) ++ fetched
1108 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1109 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1111 statuses = status_search(user, query)
1113 tags_path = Web.base_url() <> "/tag/"
1119 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1120 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1121 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1124 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1126 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1133 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1134 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1136 statuses = status_search(user, query)
1142 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1143 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1146 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1148 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1155 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1156 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1158 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1163 def favourites(%{assigns: %{user: user}} = conn, params) do
1166 |> Map.put("type", "Create")
1167 |> Map.put("favorited_by", user.ap_id)
1168 |> Map.put("blocking_user", user)
1171 ActivityPub.fetch_activities([], params)
1175 |> add_link_headers(:favourites, activities)
1176 |> put_view(StatusView)
1177 |> render("index.json", %{activities: activities, for: user, as: :activity})
1180 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1181 with %User{} = user <- User.get_by_id(id),
1182 false <- user.info.hide_favorites do
1185 |> Map.put("type", "Create")
1186 |> Map.put("favorited_by", user.ap_id)
1187 |> Map.put("blocking_user", for_user)
1191 ["https://www.w3.org/ns/activitystreams#Public"] ++
1192 [for_user.ap_id | for_user.following]
1194 ["https://www.w3.org/ns/activitystreams#Public"]
1199 |> ActivityPub.fetch_activities(params)
1203 |> add_link_headers(:favourites, activities)
1204 |> put_view(StatusView)
1205 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1208 {:error, :not_found}
1213 |> json(%{error: "Can't get favorites"})
1217 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1218 user = User.get_cached_by_id(user.id)
1221 Bookmark.for_user_query(user.id)
1222 |> Pagination.fetch_paginated(params)
1226 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1229 |> add_link_headers(:bookmarks, bookmarks)
1230 |> put_view(StatusView)
1231 |> render("index.json", %{activities: activities, for: user, as: :activity})
1234 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1235 lists = Pleroma.List.for_user(user, opts)
1236 res = ListView.render("lists.json", lists: lists)
1240 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1241 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1242 res = ListView.render("list.json", list: list)
1248 |> json(%{error: "Record not found"})
1252 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1253 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1254 res = ListView.render("lists.json", lists: lists)
1258 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1259 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1260 {:ok, _list} <- Pleroma.List.delete(list) do
1268 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1269 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1270 res = ListView.render("list.json", list: list)
1275 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1277 |> Enum.each(fn account_id ->
1278 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1279 %User{} = followed <- User.get_cached_by_id(account_id) do
1280 Pleroma.List.follow(list, followed)
1287 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1289 |> Enum.each(fn account_id ->
1290 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1291 %User{} = followed <- User.get_cached_by_id(account_id) do
1292 Pleroma.List.unfollow(list, followed)
1299 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1301 {:ok, users} = Pleroma.List.get_following(list) do
1303 |> put_view(AccountView)
1304 |> render("accounts.json", %{for: user, users: users, as: :user})
1308 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1309 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1310 {:ok, list} <- Pleroma.List.rename(list, title) do
1311 res = ListView.render("list.json", list: list)
1319 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1320 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1323 |> Map.put("type", "Create")
1324 |> Map.put("blocking_user", user)
1325 |> Map.put("muting_user", user)
1327 # we must filter the following list for the user to avoid leaking statuses the user
1328 # does not actually have permission to see (for more info, peruse security issue #270).
1331 |> Enum.filter(fn x -> x in user.following end)
1332 |> ActivityPub.fetch_activities_bounded(following, params)
1336 |> put_view(StatusView)
1337 |> render("index.json", %{activities: activities, for: user, as: :activity})
1342 |> json(%{error: "Error."})
1346 def index(%{assigns: %{user: user}} = conn, _params) do
1347 token = get_session(conn, :oauth_token)
1350 mastodon_emoji = mastodonized_emoji()
1352 limit = Config.get([:instance, :limit])
1355 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1360 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1361 access_token: token,
1363 domain: Pleroma.Web.Endpoint.host(),
1366 unfollow_modal: false,
1369 auto_play_gif: false,
1370 display_sensitive_media: false,
1371 reduce_motion: false,
1372 max_toot_chars: limit,
1373 mascot: User.get_mascot(user)["url"]
1376 delete_others_notice: present?(user.info.is_moderator),
1377 admin: present?(user.info.is_admin)
1381 default_privacy: user.info.default_scope,
1382 default_sensitive: false,
1383 allow_content_types: Config.get([:instance, :allowed_post_formats])
1385 media_attachments: %{
1386 accept_content_types: [
1402 user.info.settings ||
1432 push_subscription: nil,
1434 custom_emojis: mastodon_emoji,
1440 |> put_layout(false)
1441 |> put_view(MastodonView)
1442 |> render("index.html", %{initial_state: initial_state})
1445 |> put_session(:return_to, conn.request_path)
1446 |> redirect(to: "/web/login")
1450 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1451 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1453 with changeset <- Ecto.Changeset.change(user),
1454 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1455 {:ok, _user} <- User.update_and_set_cache(changeset) do
1460 |> put_resp_content_type("application/json")
1461 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1465 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1466 redirect(conn, to: local_mastodon_root_path(conn))
1469 @doc "Local Mastodon FE login init action"
1470 def login(conn, %{"code" => auth_token}) do
1471 with {:ok, app} <- get_or_make_app(),
1472 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1473 {:ok, token} <- Token.exchange_token(app, auth) do
1475 |> put_session(:oauth_token, token.token)
1476 |> redirect(to: local_mastodon_root_path(conn))
1480 @doc "Local Mastodon FE callback action"
1481 def login(conn, _) do
1482 with {:ok, app} <- get_or_make_app() do
1487 response_type: "code",
1488 client_id: app.client_id,
1490 scope: Enum.join(app.scopes, " ")
1493 redirect(conn, to: path)
1497 defp local_mastodon_root_path(conn) do
1498 case get_session(conn, :return_to) do
1500 mastodon_api_path(conn, :index, ["getting-started"])
1503 delete_session(conn, :return_to)
1508 defp get_or_make_app do
1509 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1510 scopes = ["read", "write", "follow", "push"]
1512 with %App{} = app <- Repo.get_by(App, find_attrs) do
1514 if app.scopes == scopes do
1518 |> Ecto.Changeset.change(%{scopes: scopes})
1526 App.register_changeset(
1528 Map.put(find_attrs, :scopes, scopes)
1535 def logout(conn, _) do
1538 |> redirect(to: "/")
1541 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1542 Logger.debug("Unimplemented, returning unmodified relationship")
1544 with %User{} = target <- User.get_cached_by_id(id) do
1546 |> put_view(AccountView)
1547 |> render("relationship.json", %{user: user, target: target})
1551 def empty_array(conn, _) do
1552 Logger.debug("Unimplemented, returning an empty array")
1556 def empty_object(conn, _) do
1557 Logger.debug("Unimplemented, returning an empty object")
1561 def get_filters(%{assigns: %{user: user}} = conn, _) do
1562 filters = Filter.get_filters(user)
1563 res = FilterView.render("filters.json", filters: filters)
1568 %{assigns: %{user: user}} = conn,
1569 %{"phrase" => phrase, "context" => context} = params
1575 hide: Map.get(params, "irreversible", false),
1576 whole_word: Map.get(params, "boolean", true)
1580 {:ok, response} = Filter.create(query)
1581 res = FilterView.render("filter.json", filter: response)
1585 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1586 filter = Filter.get(filter_id, user)
1587 res = FilterView.render("filter.json", filter: filter)
1592 %{assigns: %{user: user}} = conn,
1593 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1597 filter_id: filter_id,
1600 hide: Map.get(params, "irreversible", nil),
1601 whole_word: Map.get(params, "boolean", true)
1605 {:ok, response} = Filter.update(query)
1606 res = FilterView.render("filter.json", filter: response)
1610 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1613 filter_id: filter_id
1616 {:ok, _} = Filter.delete(query)
1622 def errors(conn, {:error, %Changeset{} = changeset}) do
1625 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1626 |> Enum.map_join(", ", fn {_k, v} -> v end)
1630 |> json(%{error: error_message})
1633 def errors(conn, {:error, :not_found}) do
1636 |> json(%{error: "Record not found"})
1639 def errors(conn, _) do
1642 |> json("Something went wrong")
1645 def suggestions(%{assigns: %{user: user}} = conn, _) do
1646 suggestions = Config.get(:suggestions)
1648 if Keyword.get(suggestions, :enabled, false) do
1649 api = Keyword.get(suggestions, :third_party_engine, "")
1650 timeout = Keyword.get(suggestions, :timeout, 5000)
1651 limit = Keyword.get(suggestions, :limit, 23)
1653 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1655 user = user.nickname
1659 |> String.replace("{{host}}", host)
1660 |> String.replace("{{user}}", user)
1662 with {:ok, %{status: 200, body: body}} <-
1667 recv_timeout: timeout,
1671 {:ok, data} <- Jason.decode(body) do
1674 |> Enum.slice(0, limit)
1679 case User.get_or_fetch(x["acct"]) do
1680 {:ok, %User{id: id}} -> id
1686 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1689 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1695 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1702 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1703 with %Activity{} = activity <- Activity.get_by_id(status_id),
1704 true <- Visibility.visible_for_user?(activity, user) do
1708 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1718 def reports(%{assigns: %{user: user}} = conn, params) do
1719 case CommonAPI.report(user, params) do
1722 |> put_view(ReportView)
1723 |> try_render("report.json", %{activity: activity})
1727 |> put_status(:bad_request)
1728 |> json(%{error: err})
1732 def account_register(
1733 %{assigns: %{app: app}} = conn,
1734 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1742 "captcha_answer_data",
1746 |> Map.put("nickname", nickname)
1747 |> Map.put("fullname", params["fullname"] || nickname)
1748 |> Map.put("bio", params["bio"] || "")
1749 |> Map.put("confirm", params["password"])
1751 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1752 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1754 token_type: "Bearer",
1755 access_token: token.token,
1757 created_at: Token.Utils.format_created_at(token)
1763 |> json(Jason.encode!(errors))
1767 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1770 |> json(%{error: "Missing parameters"})
1773 def account_register(conn, _) do
1776 |> json(%{error: "Invalid credentials"})
1779 def conversations(%{assigns: %{user: user}} = conn, params) do
1780 participations = Participation.for_user_with_last_activity_id(user, params)
1783 Enum.map(participations, fn participation ->
1784 ConversationView.render("participation.json", %{participation: participation, user: user})
1788 |> add_link_headers(:conversations, participations)
1789 |> json(conversations)
1792 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1793 with %Participation{} = participation <-
1794 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1795 {:ok, participation} <- Participation.mark_as_read(participation) do
1796 participation_view =
1797 ConversationView.render("participation.json", %{participation: participation, user: user})
1800 |> json(participation_view)
1804 def try_render(conn, target, params)
1805 when is_binary(target) do
1806 res = render(conn, target, params)
1811 |> json(%{error: "Can't display this activity"})
1817 def try_render(conn, _, _) do
1820 |> json(%{error: "Can't display this activity"})
1823 defp present?(nil), do: false
1824 defp present?(false), do: false
1825 defp present?(_), do: true