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 <- Repo.get(User, 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 <- Repo.get(Activity, 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 <- Repo.get(Activity, 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 <- Repo.get(Activity, 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 <- Repo.get(Activity, 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}}} <- Repo.get(Activity, 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}}} <- Repo.get(Activity, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(User, 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 = Repo.get(User, 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 <- Repo.get(User, 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 <- Repo.get(Pleroma.User, 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(conn, %{"code" => code}) do
1254 with {:ok, app} <- get_or_make_app(),
1255 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1256 {:ok, token} <- Token.exchange_token(app, auth) do
1258 |> put_session(:oauth_token, token.token)
1259 |> redirect(to: "/web/getting-started")
1263 def login(conn, _) do
1264 with {:ok, app} <- get_or_make_app() do
1269 response_type: "code",
1270 client_id: app.client_id,
1272 scope: Enum.join(app.scopes, " ")
1276 |> redirect(to: path)
1280 defp get_or_make_app do
1281 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1282 scopes = ["read", "write", "follow", "push"]
1284 with %App{} = app <- Repo.get_by(App, find_attrs) do
1286 if app.scopes == scopes do
1290 |> Ecto.Changeset.change(%{scopes: scopes})
1298 App.register_changeset(
1300 Map.put(find_attrs, :scopes, scopes)
1307 def logout(conn, _) do
1310 |> redirect(to: "/")
1313 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1314 Logger.debug("Unimplemented, returning unmodified relationship")
1316 with %User{} = target <- Repo.get(User, id) do
1318 |> put_view(AccountView)
1319 |> render("relationship.json", %{user: user, target: target})
1323 def empty_array(conn, _) do
1324 Logger.debug("Unimplemented, returning an empty array")
1328 def empty_object(conn, _) do
1329 Logger.debug("Unimplemented, returning an empty object")
1333 def get_filters(%{assigns: %{user: user}} = conn, _) do
1334 filters = Filter.get_filters(user)
1335 res = FilterView.render("filters.json", filters: filters)
1340 %{assigns: %{user: user}} = conn,
1341 %{"phrase" => phrase, "context" => context} = params
1347 hide: Map.get(params, "irreversible", nil),
1348 whole_word: Map.get(params, "boolean", true)
1352 {:ok, response} = Filter.create(query)
1353 res = FilterView.render("filter.json", filter: response)
1357 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1358 filter = Filter.get(filter_id, user)
1359 res = FilterView.render("filter.json", filter: filter)
1364 %{assigns: %{user: user}} = conn,
1365 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1369 filter_id: filter_id,
1372 hide: Map.get(params, "irreversible", nil),
1373 whole_word: Map.get(params, "boolean", true)
1377 {:ok, response} = Filter.update(query)
1378 res = FilterView.render("filter.json", filter: response)
1382 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1385 filter_id: filter_id
1388 {:ok, _} = Filter.delete(query)
1394 def errors(conn, _) do
1397 |> json("Something went wrong")
1400 def suggestions(%{assigns: %{user: user}} = conn, _) do
1401 suggestions = Config.get(:suggestions)
1403 if Keyword.get(suggestions, :enabled, false) do
1404 api = Keyword.get(suggestions, :third_party_engine, "")
1405 timeout = Keyword.get(suggestions, :timeout, 5000)
1406 limit = Keyword.get(suggestions, :limit, 23)
1408 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1410 user = user.nickname
1414 |> String.replace("{{host}}", host)
1415 |> String.replace("{{user}}", user)
1417 with {:ok, %{status: 200, body: body}} <-
1422 recv_timeout: timeout,
1426 {:ok, data} <- Jason.decode(body) do
1429 |> Enum.slice(0, limit)
1434 case User.get_or_fetch(x["acct"]) do
1441 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1444 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1450 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1457 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1458 with %Activity{} = activity <- Repo.get(Activity, status_id),
1459 true <- Visibility.visible_for_user?(activity, user) do
1463 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1473 def reports(%{assigns: %{user: user}} = conn, params) do
1474 case CommonAPI.report(user, params) do
1477 |> put_view(ReportView)
1478 |> try_render("report.json", %{activity: activity})
1482 |> put_status(:bad_request)
1483 |> json(%{error: err})
1487 def try_render(conn, target, params)
1488 when is_binary(target) do
1489 res = render(conn, target, params)
1494 |> json(%{error: "Can't display this activity"})
1500 def try_render(conn, _, _) do
1503 |> json(%{error: "Can't display this activity"})
1506 defp present?(nil), do: false
1507 defp present?(false), do: false
1508 defp present?(_), do: true