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.Notification
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.AppView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonView
26 alias Pleroma.Web.MastodonAPI.NotificationView
27 alias Pleroma.Web.MastodonAPI.ReportView
28 alias Pleroma.Web.MastodonAPI.StatusView
29 alias Pleroma.Web.MediaProxy
30 alias Pleroma.Web.OAuth.App
31 alias Pleroma.Web.OAuth.Authorization
32 alias Pleroma.Web.OAuth.Token
34 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
39 @httpoison Application.get_env(:pleroma, :httpoison)
40 @local_mastodon_name "Mastodon-Local"
42 action_fallback(:errors)
44 def create_app(conn, params) do
45 scopes = oauth_scopes(params, ["read"])
49 |> Map.drop(["scope", "scopes"])
50 |> Map.put("scopes", scopes)
52 with cs <- App.register_changeset(%App{}, app_attrs),
53 false <- cs.changes[:client_name] == @local_mastodon_name,
54 {:ok, app} <- Repo.insert(cs) do
57 |> render("show.json", %{app: app})
66 value_function \\ fn x -> {:ok, x} end
68 if Map.has_key?(params, params_field) do
69 case value_function.(params[params_field]) do
70 {:ok, new_value} -> Map.put(map, map_field, new_value)
78 def update_credentials(%{assigns: %{user: user}} = conn, params) do
83 |> add_if_present(params, "display_name", :name)
84 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
85 |> add_if_present(params, "avatar", :avatar, fn value ->
86 with %Plug.Upload{} <- value,
87 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
96 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
97 |> add_if_present(params, "header", :banner, fn value ->
98 with %Plug.Upload{} <- value,
99 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
106 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
108 with changeset <- User.update_changeset(user, user_params),
109 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
110 {:ok, user} <- User.update_and_set_cache(changeset) do
111 if original_user != user do
112 CommonAPI.update(user)
115 json(conn, AccountView.render("account.json", %{user: user, for: user}))
120 |> json(%{error: "Invalid request"})
124 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
125 account = AccountView.render("account.json", %{user: user, for: user})
129 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
130 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
133 |> render("short.json", %{app: app})
137 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
138 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
139 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
140 account = AccountView.render("account.json", %{user: user, for: for_user})
146 |> json(%{error: "Can't find user"})
150 @mastodon_api_level "2.5.0"
152 def masto_instance(conn, _params) do
153 instance = Config.get(:instance)
157 title: Keyword.get(instance, :name),
158 description: Keyword.get(instance, :description),
159 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
160 email: Keyword.get(instance, :email),
162 streaming_api: Pleroma.Web.Endpoint.websocket_url()
164 stats: Stats.get_stats(),
165 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
167 registrations: Pleroma.Config.get([:instance, :registrations_open]),
168 # Extra (not present in Mastodon):
169 max_toot_chars: Keyword.get(instance, :limit)
175 def peers(conn, _params) do
176 json(conn, Stats.get_peers())
179 defp mastodonized_emoji do
180 Pleroma.Emoji.get_all()
181 |> Enum.map(fn {shortcode, relative_url, tags} ->
182 url = to_string(URI.merge(Web.base_url(), relative_url))
185 "shortcode" => shortcode,
187 "visible_in_picker" => true,
189 "tags" => String.split(tags, ",")
194 def custom_emojis(conn, _params) do
195 mastodon_emoji = mastodonized_emoji()
196 json(conn, mastodon_emoji)
199 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
202 |> Map.drop(["since_id", "max_id"])
205 last = List.last(activities)
206 first = List.first(activities)
212 {next_url, prev_url} =
216 Pleroma.Web.Endpoint,
219 Map.merge(params, %{max_id: min})
222 Pleroma.Web.Endpoint,
225 Map.merge(params, %{since_id: max})
231 Pleroma.Web.Endpoint,
233 Map.merge(params, %{max_id: min})
236 Pleroma.Web.Endpoint,
238 Map.merge(params, %{since_id: max})
244 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
250 def home_timeline(%{assigns: %{user: user}} = conn, params) do
253 |> Map.put("type", ["Create", "Announce"])
254 |> Map.put("blocking_user", user)
255 |> Map.put("muting_user", user)
256 |> Map.put("user", user)
259 [user.ap_id | user.following]
260 |> ActivityPub.fetch_activities(params)
261 |> ActivityPub.contain_timeline(user)
265 |> add_link_headers(:home_timeline, activities)
266 |> put_view(StatusView)
267 |> render("index.json", %{activities: activities, for: user, as: :activity})
270 def public_timeline(%{assigns: %{user: user}} = conn, params) do
271 local_only = params["local"] in [true, "True", "true", "1"]
275 |> Map.put("type", ["Create", "Announce"])
276 |> Map.put("local_only", local_only)
277 |> Map.put("blocking_user", user)
278 |> Map.put("muting_user", user)
279 |> ActivityPub.fetch_public_activities()
283 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
284 |> put_view(StatusView)
285 |> render("index.json", %{activities: activities, for: user, as: :activity})
288 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
289 with %User{} = user <- User.get_by_id(params["id"]) do
290 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
293 |> add_link_headers(:user_statuses, activities, params["id"])
294 |> put_view(StatusView)
295 |> render("index.json", %{
296 activities: activities,
303 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
306 |> Map.put("type", "Create")
307 |> Map.put("blocking_user", user)
308 |> Map.put("user", user)
309 |> Map.put(:visibility, "direct")
313 |> ActivityPub.fetch_activities_query(params)
317 |> add_link_headers(:dm_timeline, activities)
318 |> put_view(StatusView)
319 |> render("index.json", %{activities: activities, for: user, as: :activity})
322 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
323 with %Activity{} = activity <- Activity.get_by_id(id),
324 true <- Visibility.visible_for_user?(activity, user) do
326 |> put_view(StatusView)
327 |> try_render("status.json", %{activity: activity, for: user})
331 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
332 with %Activity{} = activity <- Activity.get_by_id(id),
334 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
335 "blocking_user" => user,
339 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
341 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
342 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
348 activities: grouped_activities[true] || [],
352 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
357 activities: grouped_activities[false] || [],
361 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
368 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
369 when length(media_ids) > 0 do
372 |> Map.put("status", ".")
374 post_status(conn, params)
377 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
380 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
383 case get_req_header(conn, "idempotency-key") do
385 _ -> Ecto.UUID.generate()
389 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
392 |> put_view(StatusView)
393 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
396 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
397 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
403 |> json(%{error: "Can't delete this post"})
407 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
408 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
410 |> put_view(StatusView)
411 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
415 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
416 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
417 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
419 |> put_view(StatusView)
420 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
424 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
425 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
426 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
428 |> put_view(StatusView)
429 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
433 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
434 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
435 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
437 |> put_view(StatusView)
438 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
442 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
443 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
445 |> put_view(StatusView)
446 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
450 |> put_resp_content_type("application/json")
451 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
455 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
456 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
458 |> put_view(StatusView)
459 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
463 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
464 with %Activity{} = activity <- Activity.get_by_id(id),
465 %User{} = user <- User.get_by_nickname(user.nickname),
466 true <- Visibility.visible_for_user?(activity, user),
467 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
469 |> put_view(StatusView)
470 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
474 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
475 with %Activity{} = activity <- Activity.get_by_id(id),
476 %User{} = user <- User.get_by_nickname(user.nickname),
477 true <- Visibility.visible_for_user?(activity, user),
478 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
480 |> put_view(StatusView)
481 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
485 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
486 activity = Activity.get_by_id(id)
488 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
495 |> put_resp_content_type("application/json")
496 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
500 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
501 activity = Activity.get_by_id(id)
503 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
505 |> put_view(StatusView)
506 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
510 def notifications(%{assigns: %{user: user}} = conn, params) do
511 notifications = MastodonAPI.get_notifications(user, params)
514 |> add_link_headers(:notifications, notifications)
515 |> put_view(NotificationView)
516 |> render("index.json", %{notifications: notifications, for: user})
519 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
520 with {:ok, notification} <- Notification.get(user, id) do
522 |> put_view(NotificationView)
523 |> render("show.json", %{notification: notification, for: user})
527 |> put_resp_content_type("application/json")
528 |> send_resp(403, Jason.encode!(%{"error" => reason}))
532 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
533 Notification.clear(user)
537 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
538 with {:ok, _notif} <- Notification.dismiss(user, id) do
543 |> put_resp_content_type("application/json")
544 |> send_resp(403, Jason.encode!(%{"error" => reason}))
548 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
550 q = from(u in User, where: u.id in ^id)
551 targets = Repo.all(q)
554 |> put_view(AccountView)
555 |> render("relationships.json", %{user: user, targets: targets})
558 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
559 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
561 def update_media(%{assigns: %{user: user}} = conn, data) do
562 with %Object{} = object <- Repo.get(Object, data["id"]),
563 true <- Object.authorize_mutation(object, user),
564 true <- is_binary(data["description"]),
565 description <- data["description"] do
566 new_data = %{object.data | "name" => description}
570 |> Object.change(%{data: new_data})
573 attachment_data = Map.put(new_data, "id", object.id)
576 |> put_view(StatusView)
577 |> render("attachment.json", %{attachment: attachment_data})
581 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
582 with {:ok, object} <-
585 actor: User.ap_id(user),
586 description: Map.get(data, "description")
588 attachment_data = Map.put(object.data, "id", object.id)
591 |> put_view(StatusView)
592 |> render("attachment.json", %{attachment: attachment_data})
596 def favourited_by(conn, %{"id" => id}) do
597 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
598 q = from(u in User, where: u.ap_id in ^likes)
602 |> put_view(AccountView)
603 |> render(AccountView, "accounts.json", %{users: users, as: :user})
609 def reblogged_by(conn, %{"id" => id}) do
610 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
611 q = from(u in User, where: u.ap_id in ^announces)
615 |> put_view(AccountView)
616 |> render("accounts.json", %{users: users, as: :user})
622 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
623 local_only = params["local"] in [true, "True", "true", "1"]
626 [params["tag"], params["any"]]
630 |> Enum.map(&String.downcase(&1))
635 |> Enum.map(&String.downcase(&1))
640 |> Enum.map(&String.downcase(&1))
644 |> Map.put("type", "Create")
645 |> Map.put("local_only", local_only)
646 |> Map.put("blocking_user", user)
647 |> Map.put("muting_user", user)
648 |> Map.put("tag", tags)
649 |> Map.put("tag_all", tag_all)
650 |> Map.put("tag_reject", tag_reject)
651 |> ActivityPub.fetch_public_activities()
655 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
656 |> put_view(StatusView)
657 |> render("index.json", %{activities: activities, for: user, as: :activity})
660 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
661 with %User{} = user <- User.get_by_id(id),
662 followers <- MastodonAPI.get_followers(user, params) do
665 for_user && user.id == for_user.id -> followers
666 user.info.hide_followers -> []
671 |> add_link_headers(:followers, followers, user)
672 |> put_view(AccountView)
673 |> render("accounts.json", %{users: followers, as: :user})
677 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
678 with %User{} = user <- User.get_by_id(id),
679 followers <- MastodonAPI.get_friends(user, params) do
682 for_user && user.id == for_user.id -> followers
683 user.info.hide_follows -> []
688 |> add_link_headers(:following, followers, user)
689 |> put_view(AccountView)
690 |> render("accounts.json", %{users: followers, as: :user})
694 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
695 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
697 |> put_view(AccountView)
698 |> render("accounts.json", %{users: follow_requests, as: :user})
702 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
703 with %User{} = follower <- User.get_by_id(id),
704 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
706 |> put_view(AccountView)
707 |> render("relationship.json", %{user: followed, target: follower})
711 |> put_resp_content_type("application/json")
712 |> send_resp(403, Jason.encode!(%{"error" => message}))
716 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
717 with %User{} = follower <- User.get_by_id(id),
718 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
720 |> put_view(AccountView)
721 |> render("relationship.json", %{user: followed, target: follower})
725 |> put_resp_content_type("application/json")
726 |> send_resp(403, Jason.encode!(%{"error" => message}))
730 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
731 with %User{} = followed <- User.get_by_id(id),
732 false <- User.following?(follower, followed),
733 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
735 |> put_view(AccountView)
736 |> render("relationship.json", %{user: follower, target: followed})
739 followed = User.get_cached_by_id(id)
742 case conn.params["reblogs"] do
743 true -> CommonAPI.show_reblogs(follower, followed)
744 false -> CommonAPI.hide_reblogs(follower, followed)
748 |> put_view(AccountView)
749 |> render("relationship.json", %{user: follower, target: followed})
753 |> put_resp_content_type("application/json")
754 |> send_resp(403, Jason.encode!(%{"error" => message}))
758 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
759 with %User{} = followed <- Repo.get_by(User, nickname: uri),
760 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
762 |> put_view(AccountView)
763 |> render("account.json", %{user: followed, for: follower})
767 |> put_resp_content_type("application/json")
768 |> send_resp(403, Jason.encode!(%{"error" => message}))
772 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
773 with %User{} = followed <- User.get_by_id(id),
774 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
776 |> put_view(AccountView)
777 |> render("relationship.json", %{user: follower, target: followed})
781 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
782 with %User{} = muted <- User.get_by_id(id),
783 {:ok, muter} <- User.mute(muter, muted) do
785 |> put_view(AccountView)
786 |> render("relationship.json", %{user: muter, target: muted})
790 |> put_resp_content_type("application/json")
791 |> send_resp(403, Jason.encode!(%{"error" => message}))
795 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
796 with %User{} = muted <- User.get_by_id(id),
797 {:ok, muter} <- User.unmute(muter, muted) do
799 |> put_view(AccountView)
800 |> render("relationship.json", %{user: muter, target: muted})
804 |> put_resp_content_type("application/json")
805 |> send_resp(403, Jason.encode!(%{"error" => message}))
809 def mutes(%{assigns: %{user: user}} = conn, _) do
810 with muted_accounts <- User.muted_users(user) do
811 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
816 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
817 with %User{} = blocked <- User.get_by_id(id),
818 {:ok, blocker} <- User.block(blocker, blocked),
819 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
821 |> put_view(AccountView)
822 |> render("relationship.json", %{user: blocker, target: blocked})
826 |> put_resp_content_type("application/json")
827 |> send_resp(403, Jason.encode!(%{"error" => message}))
831 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
832 with %User{} = blocked <- User.get_by_id(id),
833 {:ok, blocker} <- User.unblock(blocker, blocked),
834 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
836 |> put_view(AccountView)
837 |> render("relationship.json", %{user: blocker, target: blocked})
841 |> put_resp_content_type("application/json")
842 |> send_resp(403, Jason.encode!(%{"error" => message}))
846 def blocks(%{assigns: %{user: user}} = conn, _) do
847 with blocked_accounts <- User.blocked_users(user) do
848 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
853 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
854 json(conn, info.domain_blocks || [])
857 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
858 User.block_domain(blocker, domain)
862 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
863 User.unblock_domain(blocker, domain)
867 def status_search(user, query) do
869 if Regex.match?(~r/https?:/, query) do
870 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
871 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
872 true <- Visibility.visible_for_user?(activity, user) do
882 where: fragment("?->>'type' = 'Create'", a.data),
883 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
886 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
891 order_by: [desc: :id]
894 Repo.all(q) ++ fetched
897 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
898 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
900 statuses = status_search(user, query)
902 tags_path = Web.base_url() <> "/tag/"
908 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
909 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
910 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
913 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
915 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
922 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
923 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
925 statuses = status_search(user, query)
931 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
932 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
935 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
937 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
944 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
945 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
947 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
952 def favourites(%{assigns: %{user: user}} = conn, params) do
955 |> Map.put("type", "Create")
956 |> Map.put("favorited_by", user.ap_id)
957 |> Map.put("blocking_user", user)
960 ActivityPub.fetch_activities([], params)
964 |> add_link_headers(:favourites, activities)
965 |> put_view(StatusView)
966 |> render("index.json", %{activities: activities, for: user, as: :activity})
969 def bookmarks(%{assigns: %{user: user}} = conn, _) do
970 user = User.get_by_id(user.id)
974 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
978 |> put_view(StatusView)
979 |> render("index.json", %{activities: activities, for: user, as: :activity})
982 def get_lists(%{assigns: %{user: user}} = conn, opts) do
983 lists = Pleroma.List.for_user(user, opts)
984 res = ListView.render("lists.json", lists: lists)
988 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
989 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
990 res = ListView.render("list.json", list: list)
996 |> json(%{error: "Record not found"})
1000 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1001 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1002 res = ListView.render("lists.json", lists: lists)
1006 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1007 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1008 {:ok, _list} <- Pleroma.List.delete(list) do
1016 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1017 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1018 res = ListView.render("list.json", list: list)
1023 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1025 |> Enum.each(fn account_id ->
1026 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1027 %User{} = followed <- User.get_by_id(account_id) do
1028 Pleroma.List.follow(list, followed)
1035 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1037 |> Enum.each(fn account_id ->
1038 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1039 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1040 Pleroma.List.unfollow(list, followed)
1047 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1048 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1049 {:ok, users} = Pleroma.List.get_following(list) do
1051 |> put_view(AccountView)
1052 |> render("accounts.json", %{users: users, as: :user})
1056 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1057 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1058 {:ok, list} <- Pleroma.List.rename(list, title) do
1059 res = ListView.render("list.json", list: list)
1067 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1068 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1071 |> Map.put("type", "Create")
1072 |> Map.put("blocking_user", user)
1073 |> Map.put("muting_user", user)
1075 # we must filter the following list for the user to avoid leaking statuses the user
1076 # does not actually have permission to see (for more info, peruse security issue #270).
1079 |> Enum.filter(fn x -> x in user.following end)
1080 |> ActivityPub.fetch_activities_bounded(following, params)
1084 |> put_view(StatusView)
1085 |> render("index.json", %{activities: activities, for: user, as: :activity})
1090 |> json(%{error: "Error."})
1094 def index(%{assigns: %{user: user}} = conn, _params) do
1097 |> get_session(:oauth_token)
1100 mastodon_emoji = mastodonized_emoji()
1102 limit = Config.get([:instance, :limit])
1105 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1107 flavour = get_user_flavour(user)
1112 streaming_api_base_url:
1113 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1114 access_token: token,
1116 domain: Pleroma.Web.Endpoint.host(),
1119 unfollow_modal: false,
1122 auto_play_gif: false,
1123 display_sensitive_media: false,
1124 reduce_motion: false,
1125 max_toot_chars: limit
1128 delete_others_notice: present?(user.info.is_moderator),
1129 admin: present?(user.info.is_admin)
1133 default_privacy: user.info.default_scope,
1134 default_sensitive: false,
1135 allow_content_types: Config.get([:instance, :allowed_post_formats])
1137 media_attachments: %{
1138 accept_content_types: [
1154 user.info.settings ||
1184 push_subscription: nil,
1186 custom_emojis: mastodon_emoji,
1192 |> put_layout(false)
1193 |> put_view(MastodonView)
1194 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1197 |> redirect(to: "/web/login")
1201 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1202 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1204 with changeset <- Ecto.Changeset.change(user),
1205 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1206 {:ok, _user} <- User.update_and_set_cache(changeset) do
1211 |> put_resp_content_type("application/json")
1212 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1216 @supported_flavours ["glitch", "vanilla"]
1218 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1219 when flavour in @supported_flavours do
1220 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1222 with changeset <- Ecto.Changeset.change(user),
1223 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1224 {:ok, user} <- User.update_and_set_cache(changeset),
1225 flavour <- user.info.flavour do
1230 |> put_resp_content_type("application/json")
1231 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1235 def set_flavour(conn, _params) do
1238 |> json(%{error: "Unsupported flavour"})
1241 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1242 json(conn, get_user_flavour(user))
1245 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1249 defp get_user_flavour(_) do
1253 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1254 redirect(conn, to: local_mastodon_root_path(conn))
1257 @doc "Local Mastodon FE login init action"
1258 def login(conn, %{"code" => auth_token}) do
1259 with {:ok, app} <- get_or_make_app(),
1260 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1261 {:ok, token} <- Token.exchange_token(app, auth) do
1263 |> put_session(:oauth_token, token.token)
1264 |> redirect(to: local_mastodon_root_path(conn))
1268 @doc "Local Mastodon FE callback action"
1269 def login(conn, _) do
1270 with {:ok, app} <- get_or_make_app() do
1275 response_type: "code",
1276 client_id: app.client_id,
1278 scope: Enum.join(app.scopes, " ")
1282 |> redirect(to: path)
1286 defp local_mastodon_root_path(conn), do: mastodon_api_path(conn, :index, ["getting-started"])
1288 defp get_or_make_app do
1289 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1290 scopes = ["read", "write", "follow", "push"]
1292 with %App{} = app <- Repo.get_by(App, find_attrs) do
1294 if app.scopes == scopes do
1298 |> Ecto.Changeset.change(%{scopes: scopes})
1306 App.register_changeset(
1308 Map.put(find_attrs, :scopes, scopes)
1315 def logout(conn, _) do
1318 |> redirect(to: "/")
1321 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1322 Logger.debug("Unimplemented, returning unmodified relationship")
1324 with %User{} = target <- User.get_by_id(id) do
1326 |> put_view(AccountView)
1327 |> render("relationship.json", %{user: user, target: target})
1331 def empty_array(conn, _) do
1332 Logger.debug("Unimplemented, returning an empty array")
1336 def empty_object(conn, _) do
1337 Logger.debug("Unimplemented, returning an empty object")
1341 def get_filters(%{assigns: %{user: user}} = conn, _) do
1342 filters = Filter.get_filters(user)
1343 res = FilterView.render("filters.json", filters: filters)
1348 %{assigns: %{user: user}} = conn,
1349 %{"phrase" => phrase, "context" => context} = params
1355 hide: Map.get(params, "irreversible", nil),
1356 whole_word: Map.get(params, "boolean", true)
1360 {:ok, response} = Filter.create(query)
1361 res = FilterView.render("filter.json", filter: response)
1365 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1366 filter = Filter.get(filter_id, user)
1367 res = FilterView.render("filter.json", filter: filter)
1372 %{assigns: %{user: user}} = conn,
1373 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1377 filter_id: filter_id,
1380 hide: Map.get(params, "irreversible", nil),
1381 whole_word: Map.get(params, "boolean", true)
1385 {:ok, response} = Filter.update(query)
1386 res = FilterView.render("filter.json", filter: response)
1390 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1393 filter_id: filter_id
1396 {:ok, _} = Filter.delete(query)
1402 def errors(conn, _) do
1405 |> json("Something went wrong")
1408 def suggestions(%{assigns: %{user: user}} = conn, _) do
1409 suggestions = Config.get(:suggestions)
1411 if Keyword.get(suggestions, :enabled, false) do
1412 api = Keyword.get(suggestions, :third_party_engine, "")
1413 timeout = Keyword.get(suggestions, :timeout, 5000)
1414 limit = Keyword.get(suggestions, :limit, 23)
1416 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1418 user = user.nickname
1422 |> String.replace("{{host}}", host)
1423 |> String.replace("{{user}}", user)
1425 with {:ok, %{status: 200, body: body}} <-
1430 recv_timeout: timeout,
1434 {:ok, data} <- Jason.decode(body) do
1437 |> Enum.slice(0, limit)
1442 case User.get_or_fetch(x["acct"]) do
1449 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1452 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1458 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1465 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1466 with %Activity{} = activity <- Activity.get_by_id(status_id),
1467 true <- Visibility.visible_for_user?(activity, user) do
1471 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1481 def reports(%{assigns: %{user: user}} = conn, params) do
1482 case CommonAPI.report(user, params) do
1485 |> put_view(ReportView)
1486 |> try_render("report.json", %{activity: activity})
1490 |> put_status(:bad_request)
1491 |> json(%{error: err})
1495 def try_render(conn, target, params)
1496 when is_binary(target) do
1497 res = render(conn, target, params)
1502 |> json(%{error: "Can't display this activity"})
1508 def try_render(conn, _, _) do
1511 |> json(%{error: "Can't display this activity"})
1514 defp present?(nil), do: false
1515 defp present?(false), do: false
1516 defp present?(_), do: true