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
10 alias Pleroma.Notification
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MediaProxy
19 alias Pleroma.Web.MastodonAPI.AccountView
20 alias Pleroma.Web.MastodonAPI.FilterView
21 alias Pleroma.Web.MastodonAPI.ListView
22 alias Pleroma.Web.MastodonAPI.MastodonView
23 alias Pleroma.Web.MastodonAPI.StatusView
24 alias Pleroma.Web.MastodonAPI.ReportView
25 alias Pleroma.Web.ActivityPub.ActivityPub
26 alias Pleroma.Web.ActivityPub.Utils
27 alias Pleroma.Web.ActivityPub.Visibility
28 alias Pleroma.Web.OAuth.App
29 alias Pleroma.Web.OAuth.Authorization
30 alias Pleroma.Web.OAuth.Token
32 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
37 @httpoison Application.get_env(:pleroma, :httpoison)
38 @local_mastodon_name "Mastodon-Local"
40 action_fallback(:errors)
42 def create_app(conn, params) do
43 scopes = oauth_scopes(params, ["read"])
47 |> Map.drop(["scope", "scopes"])
48 |> Map.put("scopes", scopes)
50 with cs <- App.register_changeset(%App{}, app_attrs),
51 false <- cs.changes[:client_name] == @local_mastodon_name,
52 {:ok, app} <- Repo.insert(cs) do
54 id: app.id |> to_string,
55 name: app.client_name,
56 client_id: app.client_id,
57 client_secret: app.client_secret,
58 redirect_uri: app.redirect_uris,
71 value_function \\ fn x -> {:ok, x} end
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
101 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
102 |> add_if_present(params, "header", :banner, fn value ->
103 with %Plug.Upload{} <- value,
104 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
111 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113 with changeset <- User.update_changeset(user, user_params),
114 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
115 {:ok, user} <- User.update_and_set_cache(changeset) do
116 if original_user != user do
117 CommonAPI.update(user)
120 json(conn, AccountView.render("account.json", %{user: user, for: user}))
125 |> json(%{error: "Invalid request"})
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 account = AccountView.render("account.json", %{user: user, for: user})
134 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
135 with %User{} = user <- Repo.get(User, id),
136 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
137 account = AccountView.render("account.json", %{user: user, for: for_user})
143 |> json(%{error: "Can't find user"})
147 @mastodon_api_level "2.5.0"
149 def masto_instance(conn, _params) do
150 instance = Config.get(:instance)
154 title: Keyword.get(instance, :name),
155 description: Keyword.get(instance, :description),
156 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
157 email: Keyword.get(instance, :email),
159 streaming_api: Pleroma.Web.Endpoint.websocket_url()
161 stats: Stats.get_stats(),
162 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
163 max_toot_chars: Keyword.get(instance, :limit)
169 def peers(conn, _params) do
170 json(conn, Stats.get_peers())
173 defp mastodonized_emoji do
174 Pleroma.Emoji.get_all()
175 |> Enum.map(fn {shortcode, relative_url} ->
176 url = to_string(URI.merge(Web.base_url(), relative_url))
179 "shortcode" => shortcode,
181 "visible_in_picker" => true,
187 def custom_emojis(conn, _params) do
188 mastodon_emoji = mastodonized_emoji()
189 json(conn, mastodon_emoji)
192 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
193 last = List.last(activities)
194 first = List.first(activities)
200 {next_url, prev_url} =
204 Pleroma.Web.Endpoint,
207 Map.merge(params, %{max_id: min})
210 Pleroma.Web.Endpoint,
213 Map.merge(params, %{since_id: max})
219 Pleroma.Web.Endpoint,
221 Map.merge(params, %{max_id: min})
224 Pleroma.Web.Endpoint,
226 Map.merge(params, %{since_id: max})
232 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
238 def home_timeline(%{assigns: %{user: user}} = conn, params) do
241 |> Map.put("type", ["Create", "Announce"])
242 |> Map.put("blocking_user", user)
243 |> Map.put("muting_user", user)
244 |> Map.put("user", user)
247 [user.ap_id | user.following]
248 |> ActivityPub.fetch_activities(params)
249 |> ActivityPub.contain_timeline(user)
253 |> add_link_headers(:home_timeline, activities)
254 |> put_view(StatusView)
255 |> render("index.json", %{activities: activities, for: user, as: :activity})
258 def public_timeline(%{assigns: %{user: user}} = conn, params) do
259 local_only = params["local"] in [true, "True", "true", "1"]
263 |> Map.put("type", ["Create", "Announce"])
264 |> Map.put("local_only", local_only)
265 |> Map.put("blocking_user", user)
266 |> Map.put("muting_user", user)
267 |> ActivityPub.fetch_public_activities()
271 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
272 |> put_view(StatusView)
273 |> render("index.json", %{activities: activities, for: user, as: :activity})
276 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
277 with %User{} = user <- Repo.get(User, params["id"]) do
278 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
281 |> add_link_headers(:user_statuses, activities, params["id"])
282 |> put_view(StatusView)
283 |> render("index.json", %{
284 activities: activities,
291 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
294 |> Map.put("type", "Create")
295 |> Map.put("blocking_user", user)
296 |> Map.put("user", user)
297 |> Map.put(:visibility, "direct")
301 |> ActivityPub.fetch_activities_query(params)
305 |> add_link_headers(:dm_timeline, activities)
306 |> put_view(StatusView)
307 |> render("index.json", %{activities: activities, for: user, as: :activity})
310 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
311 with %Activity{} = activity <- Repo.get(Activity, id),
312 true <- Visibility.visible_for_user?(activity, user) do
314 |> put_view(StatusView)
315 |> try_render("status.json", %{activity: activity, for: user})
319 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
320 with %Activity{} = activity <- Repo.get(Activity, id),
322 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
323 "blocking_user" => user,
327 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
329 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
330 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
336 activities: grouped_activities[true] || [],
340 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
345 activities: grouped_activities[false] || [],
349 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
356 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
357 when length(media_ids) > 0 do
360 |> Map.put("status", ".")
362 post_status(conn, params)
365 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
368 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
371 case get_req_header(conn, "idempotency-key") do
373 _ -> Ecto.UUID.generate()
377 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
380 |> put_view(StatusView)
381 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
384 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
385 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
391 |> json(%{error: "Can't delete this post"})
395 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
396 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
398 |> put_view(StatusView)
399 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
403 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
404 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
405 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
407 |> put_view(StatusView)
408 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
412 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
413 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
414 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
416 |> put_view(StatusView)
417 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
421 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
422 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
423 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
425 |> put_view(StatusView)
426 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
430 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
431 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
433 |> put_view(StatusView)
434 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
438 |> put_resp_content_type("application/json")
439 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
443 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
444 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
446 |> put_view(StatusView)
447 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
451 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
452 with %Activity{} = activity <- Repo.get(Activity, id),
453 %User{} = user <- User.get_by_nickname(user.nickname),
454 true <- Visibility.visible_for_user?(activity, user),
455 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
457 |> put_view(StatusView)
458 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
462 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
463 with %Activity{} = activity <- Repo.get(Activity, id),
464 %User{} = user <- User.get_by_nickname(user.nickname),
465 true <- Visibility.visible_for_user?(activity, user),
466 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
468 |> put_view(StatusView)
469 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
473 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
474 activity = Activity.get_by_id(id)
476 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
478 |> put_view(StatusView)
479 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
483 |> put_resp_content_type("application/json")
484 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
488 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
489 activity = Activity.get_by_id(id)
491 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
493 |> put_view(StatusView)
494 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
498 def notifications(%{assigns: %{user: user}} = conn, params) do
499 notifications = Notification.for_user(user, params)
503 |> Enum.map(fn x -> render_notification(user, x) end)
507 |> add_link_headers(:notifications, notifications)
511 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
512 with {:ok, notification} <- Notification.get(user, id) do
513 json(conn, render_notification(user, notification))
517 |> put_resp_content_type("application/json")
518 |> send_resp(403, Jason.encode!(%{"error" => reason}))
522 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
523 Notification.clear(user)
527 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
528 with {:ok, _notif} <- Notification.dismiss(user, id) do
533 |> put_resp_content_type("application/json")
534 |> send_resp(403, Jason.encode!(%{"error" => reason}))
538 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
540 q = from(u in User, where: u.id in ^id)
541 targets = Repo.all(q)
544 |> put_view(AccountView)
545 |> render("relationships.json", %{user: user, targets: targets})
548 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
549 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
551 def update_media(%{assigns: %{user: user}} = conn, data) do
552 with %Object{} = object <- Repo.get(Object, data["id"]),
553 true <- Object.authorize_mutation(object, user),
554 true <- is_binary(data["description"]),
555 description <- data["description"] do
556 new_data = %{object.data | "name" => description}
560 |> Object.change(%{data: new_data})
563 attachment_data = Map.put(new_data, "id", object.id)
566 |> put_view(StatusView)
567 |> render("attachment.json", %{attachment: attachment_data})
571 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
572 with {:ok, object} <-
575 actor: User.ap_id(user),
576 description: Map.get(data, "description")
578 attachment_data = Map.put(object.data, "id", object.id)
581 |> put_view(StatusView)
582 |> render("attachment.json", %{attachment: attachment_data})
586 def favourited_by(conn, %{"id" => id}) do
587 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
588 q = from(u in User, where: u.ap_id in ^likes)
592 |> put_view(AccountView)
593 |> render(AccountView, "accounts.json", %{users: users, as: :user})
599 def reblogged_by(conn, %{"id" => id}) do
600 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
601 q = from(u in User, where: u.ap_id in ^announces)
605 |> put_view(AccountView)
606 |> render("accounts.json", %{users: users, as: :user})
612 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
613 local_only = params["local"] in [true, "True", "true", "1"]
616 [params["tag"], params["any"]]
620 |> Enum.map(&String.downcase(&1))
625 |> Enum.map(&String.downcase(&1))
630 |> Enum.map(&String.downcase(&1))
634 |> Map.put("type", "Create")
635 |> Map.put("local_only", local_only)
636 |> Map.put("blocking_user", user)
637 |> Map.put("muting_user", user)
638 |> Map.put("tag", tags)
639 |> Map.put("tag_all", tag_all)
640 |> Map.put("tag_reject", tag_reject)
641 |> ActivityPub.fetch_public_activities()
645 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
646 |> put_view(StatusView)
647 |> render("index.json", %{activities: activities, for: user, as: :activity})
650 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
651 with %User{} = user <- Repo.get(User, id),
652 {:ok, followers} <- User.get_followers(user) do
655 for_user && user.id == for_user.id -> followers
656 user.info.hide_followers -> []
661 |> put_view(AccountView)
662 |> render("accounts.json", %{users: followers, as: :user})
666 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
667 with %User{} = user <- Repo.get(User, id),
668 {:ok, followers} <- User.get_friends(user) do
671 for_user && user.id == for_user.id -> followers
672 user.info.hide_follows -> []
677 |> put_view(AccountView)
678 |> render("accounts.json", %{users: followers, as: :user})
682 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
683 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
685 |> put_view(AccountView)
686 |> render("accounts.json", %{users: follow_requests, as: :user})
690 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
691 with %User{} = follower <- Repo.get(User, id),
692 {:ok, follower} <- User.maybe_follow(follower, followed),
693 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
694 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
696 ActivityPub.accept(%{
697 to: [follower.ap_id],
699 object: follow_activity.data["id"],
703 |> put_view(AccountView)
704 |> render("relationship.json", %{user: followed, target: follower})
708 |> put_resp_content_type("application/json")
709 |> send_resp(403, Jason.encode!(%{"error" => message}))
713 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
714 with %User{} = follower <- Repo.get(User, id),
715 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
716 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
718 ActivityPub.reject(%{
719 to: [follower.ap_id],
721 object: follow_activity.data["id"],
725 |> put_view(AccountView)
726 |> render("relationship.json", %{user: followed, target: follower})
730 |> put_resp_content_type("application/json")
731 |> send_resp(403, Jason.encode!(%{"error" => message}))
735 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
736 with %User{} = followed <- Repo.get(User, id),
737 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
739 |> put_view(AccountView)
740 |> render("relationship.json", %{user: follower, target: followed})
744 |> put_resp_content_type("application/json")
745 |> send_resp(403, Jason.encode!(%{"error" => message}))
749 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
750 with %User{} = followed <- Repo.get_by(User, nickname: uri),
751 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
753 |> put_view(AccountView)
754 |> render("account.json", %{user: followed, for: follower})
758 |> put_resp_content_type("application/json")
759 |> send_resp(403, Jason.encode!(%{"error" => message}))
763 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
764 with %User{} = followed <- Repo.get(User, id),
765 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
766 {:ok, follower, _} <- User.unfollow(follower, followed) do
768 |> put_view(AccountView)
769 |> render("relationship.json", %{user: follower, target: followed})
773 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
774 with %User{} = muted <- Repo.get(User, id),
775 {:ok, muter} <- User.mute(muter, muted) do
777 |> put_view(AccountView)
778 |> render("relationship.json", %{user: muter, target: muted})
782 |> put_resp_content_type("application/json")
783 |> send_resp(403, Jason.encode!(%{"error" => message}))
787 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
788 with %User{} = muted <- Repo.get(User, id),
789 {:ok, muter} <- User.unmute(muter, muted) do
791 |> put_view(AccountView)
792 |> render("relationship.json", %{user: muter, target: muted})
796 |> put_resp_content_type("application/json")
797 |> send_resp(403, Jason.encode!(%{"error" => message}))
801 def mutes(%{assigns: %{user: user}} = conn, _) do
802 with muted_accounts <- User.muted_users(user) do
803 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
808 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
809 with %User{} = blocked <- Repo.get(User, id),
810 {:ok, blocker} <- User.block(blocker, blocked),
811 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
813 |> put_view(AccountView)
814 |> render("relationship.json", %{user: blocker, target: blocked})
818 |> put_resp_content_type("application/json")
819 |> send_resp(403, Jason.encode!(%{"error" => message}))
823 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
824 with %User{} = blocked <- Repo.get(User, id),
825 {:ok, blocker} <- User.unblock(blocker, blocked),
826 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
828 |> put_view(AccountView)
829 |> render("relationship.json", %{user: blocker, target: blocked})
833 |> put_resp_content_type("application/json")
834 |> send_resp(403, Jason.encode!(%{"error" => message}))
838 def blocks(%{assigns: %{user: user}} = conn, _) do
839 with blocked_accounts <- User.blocked_users(user) do
840 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
845 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
846 json(conn, info.domain_blocks || [])
849 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
850 User.block_domain(blocker, domain)
854 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
855 User.unblock_domain(blocker, domain)
859 def status_search(user, query) do
861 if Regex.match?(~r/https?:/, query) do
862 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
863 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
864 true <- Visibility.visible_for_user?(activity, user) do
874 where: fragment("?->>'type' = 'Create'", a.data),
875 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
878 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
883 order_by: [desc: :id]
886 Repo.all(q) ++ fetched
889 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
890 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
892 statuses = status_search(user, query)
894 tags_path = Web.base_url() <> "/tag/"
900 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
901 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
902 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
905 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
907 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
914 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
915 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
917 statuses = status_search(user, query)
923 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
924 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
927 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
929 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
936 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
937 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
939 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
944 def favourites(%{assigns: %{user: user}} = conn, params) do
947 |> Map.put("type", "Create")
948 |> Map.put("favorited_by", user.ap_id)
949 |> Map.put("blocking_user", user)
950 |> ActivityPub.fetch_public_activities()
954 |> add_link_headers(:favourites, activities)
955 |> put_view(StatusView)
956 |> render("index.json", %{activities: activities, for: user, as: :activity})
959 def bookmarks(%{assigns: %{user: user}} = conn, _) do
960 user = Repo.get(User, user.id)
964 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
968 |> put_view(StatusView)
969 |> render("index.json", %{activities: activities, for: user, as: :activity})
972 def get_lists(%{assigns: %{user: user}} = conn, opts) do
973 lists = Pleroma.List.for_user(user, opts)
974 res = ListView.render("lists.json", lists: lists)
978 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
979 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
980 res = ListView.render("list.json", list: list)
986 |> json(%{error: "Record not found"})
990 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
991 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
992 res = ListView.render("lists.json", lists: lists)
996 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
997 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
998 {:ok, _list} <- Pleroma.List.delete(list) do
1006 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1007 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1008 res = ListView.render("list.json", list: list)
1013 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1015 |> Enum.each(fn account_id ->
1016 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1017 %User{} = followed <- Repo.get(User, account_id) do
1018 Pleroma.List.follow(list, followed)
1025 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1027 |> Enum.each(fn account_id ->
1028 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1029 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1030 Pleroma.List.unfollow(list, followed)
1037 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1038 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1039 {:ok, users} = Pleroma.List.get_following(list) do
1041 |> put_view(AccountView)
1042 |> render("accounts.json", %{users: users, as: :user})
1046 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1047 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1048 {:ok, list} <- Pleroma.List.rename(list, title) do
1049 res = ListView.render("list.json", list: list)
1057 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1058 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1061 |> Map.put("type", "Create")
1062 |> Map.put("blocking_user", user)
1063 |> Map.put("muting_user", user)
1065 # we must filter the following list for the user to avoid leaking statuses the user
1066 # does not actually have permission to see (for more info, peruse security issue #270).
1069 |> Enum.filter(fn x -> x in user.following end)
1070 |> ActivityPub.fetch_activities_bounded(following, params)
1074 |> put_view(StatusView)
1075 |> render("index.json", %{activities: activities, for: user, as: :activity})
1080 |> json(%{error: "Error."})
1084 def index(%{assigns: %{user: user}} = conn, _params) do
1087 |> get_session(:oauth_token)
1090 mastodon_emoji = mastodonized_emoji()
1092 limit = Config.get([:instance, :limit])
1095 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1097 flavour = get_user_flavour(user)
1102 streaming_api_base_url:
1103 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1104 access_token: token,
1106 domain: Pleroma.Web.Endpoint.host(),
1109 unfollow_modal: false,
1112 auto_play_gif: false,
1113 display_sensitive_media: false,
1114 reduce_motion: false,
1115 max_toot_chars: limit
1118 delete_others_notice: present?(user.info.is_moderator),
1119 admin: present?(user.info.is_admin)
1123 default_privacy: user.info.default_scope,
1124 default_sensitive: false
1126 media_attachments: %{
1127 accept_content_types: [
1143 user.info.settings ||
1173 push_subscription: nil,
1175 custom_emojis: mastodon_emoji,
1181 |> put_layout(false)
1182 |> put_view(MastodonView)
1183 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1186 |> redirect(to: "/web/login")
1190 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1191 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1193 with changeset <- Ecto.Changeset.change(user),
1194 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1195 {:ok, _user} <- User.update_and_set_cache(changeset) do
1200 |> put_resp_content_type("application/json")
1201 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1205 @supported_flavours ["glitch", "vanilla"]
1207 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1208 when flavour in @supported_flavours do
1209 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1211 with changeset <- Ecto.Changeset.change(user),
1212 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1213 {:ok, user} <- User.update_and_set_cache(changeset),
1214 flavour <- user.info.flavour do
1219 |> put_resp_content_type("application/json")
1220 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1224 def set_flavour(conn, _params) do
1227 |> json(%{error: "Unsupported flavour"})
1230 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1231 json(conn, get_user_flavour(user))
1234 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1238 defp get_user_flavour(_) do
1242 def login(conn, %{"code" => code}) do
1243 with {:ok, app} <- get_or_make_app(),
1244 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1245 {:ok, token} <- Token.exchange_token(app, auth) do
1247 |> put_session(:oauth_token, token.token)
1248 |> redirect(to: "/web/getting-started")
1252 def login(conn, _) do
1253 with {:ok, app} <- get_or_make_app() do
1258 response_type: "code",
1259 client_id: app.client_id,
1261 scope: Enum.join(app.scopes, " ")
1265 |> redirect(to: path)
1269 defp get_or_make_app() do
1270 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1271 scopes = ["read", "write", "follow", "push"]
1273 with %App{} = app <- Repo.get_by(App, find_attrs) do
1275 if app.scopes == scopes do
1279 |> Ecto.Changeset.change(%{scopes: scopes})
1287 App.register_changeset(
1289 Map.put(find_attrs, :scopes, scopes)
1296 def logout(conn, _) do
1299 |> redirect(to: "/")
1302 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1303 Logger.debug("Unimplemented, returning unmodified relationship")
1305 with %User{} = target <- Repo.get(User, id) do
1307 |> put_view(AccountView)
1308 |> render("relationship.json", %{user: user, target: target})
1312 def empty_array(conn, _) do
1313 Logger.debug("Unimplemented, returning an empty array")
1317 def empty_object(conn, _) do
1318 Logger.debug("Unimplemented, returning an empty object")
1322 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1323 actor = User.get_cached_by_ap_id(activity.data["actor"])
1324 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1325 mastodon_type = Activity.mastodon_notification_type(activity)
1329 type: mastodon_type,
1330 created_at: CommonAPI.Utils.to_masto_date(created_at),
1331 account: AccountView.render("account.json", %{user: actor, for: user})
1334 case mastodon_type do
1338 status: StatusView.render("status.json", %{activity: activity, for: user})
1344 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1350 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1361 def get_filters(%{assigns: %{user: user}} = conn, _) do
1362 filters = Filter.get_filters(user)
1363 res = FilterView.render("filters.json", filters: filters)
1368 %{assigns: %{user: user}} = conn,
1369 %{"phrase" => phrase, "context" => context} = params
1375 hide: Map.get(params, "irreversible", nil),
1376 whole_word: Map.get(params, "boolean", true)
1380 {:ok, response} = Filter.create(query)
1381 res = FilterView.render("filter.json", filter: response)
1385 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1386 filter = Filter.get(filter_id, user)
1387 res = FilterView.render("filter.json", filter: filter)
1392 %{assigns: %{user: user}} = conn,
1393 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1397 filter_id: filter_id,
1400 hide: Map.get(params, "irreversible", nil),
1401 whole_word: Map.get(params, "boolean", true)
1405 {:ok, response} = Filter.update(query)
1406 res = FilterView.render("filter.json", filter: response)
1410 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1413 filter_id: filter_id
1416 {:ok, _} = Filter.delete(query)
1422 def errors(conn, _) do
1425 |> json("Something went wrong")
1428 def suggestions(%{assigns: %{user: user}} = conn, _) do
1429 suggestions = Config.get(:suggestions)
1431 if Keyword.get(suggestions, :enabled, false) do
1432 api = Keyword.get(suggestions, :third_party_engine, "")
1433 timeout = Keyword.get(suggestions, :timeout, 5000)
1434 limit = Keyword.get(suggestions, :limit, 23)
1436 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1438 user = user.nickname
1442 |> String.replace("{{host}}", host)
1443 |> String.replace("{{user}}", user)
1445 with {:ok, %{status: 200, body: body}} <-
1451 recv_timeout: timeout,
1455 {:ok, data} <- Jason.decode(body) do
1458 |> Enum.slice(0, limit)
1463 case User.get_or_fetch(x["acct"]) do
1470 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1473 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1479 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1486 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1487 with %Activity{} = activity <- Repo.get(Activity, status_id),
1488 true <- Visibility.visible_for_user?(activity, user) do
1492 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1502 def reports(%{assigns: %{user: user}} = conn, params) do
1503 case CommonAPI.report(user, params) do
1506 |> put_view(ReportView)
1507 |> try_render("report.json", %{activity: activity})
1511 |> put_status(:bad_request)
1512 |> json(%{error: err})
1516 def try_render(conn, target, params)
1517 when is_binary(target) do
1518 res = render(conn, target, params)
1523 |> json(%{error: "Can't display this activity"})
1529 def try_render(conn, _, _) do
1532 |> json(%{error: "Can't display this activity"})
1535 defp present?(nil), do: false
1536 defp present?(false), do: false
1537 defp present?(_), do: true