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
18 alias Pleroma.Web.Push
19 alias Push.Subscription
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.PushSubscriptionView
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Utils
29 alias Pleroma.Web.OAuth.App
30 alias Pleroma.Web.OAuth.Authorization
31 alias Pleroma.Web.OAuth.Token
33 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
38 @httpoison Application.get_env(:pleroma, :httpoison)
39 @local_mastodon_name "Mastodon-Local"
41 action_fallback(:errors)
43 def create_app(conn, params) do
44 scopes = oauth_scopes(params, ["read"])
48 |> Map.drop(["scope", "scopes"])
49 |> Map.put("scopes", scopes)
51 with cs <- App.register_changeset(%App{}, app_attrs),
52 false <- cs.changes[:client_name] == @local_mastodon_name,
53 {:ok, app} <- Repo.insert(cs) do
55 id: app.id |> to_string,
56 name: app.client_name,
57 client_id: app.client_id,
58 client_secret: app.client_secret,
59 redirect_uri: app.redirect_uris,
72 value_function \\ fn x -> {:ok, x} end
74 if Map.has_key?(params, params_field) do
75 case value_function.(params[params_field]) do
76 {:ok, new_value} -> Map.put(map, map_field, new_value)
84 def update_credentials(%{assigns: %{user: user}} = conn, params) do
89 |> add_if_present(params, "display_name", :name)
90 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
91 |> add_if_present(params, "avatar", :avatar, fn value ->
92 with %Plug.Upload{} <- value,
93 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
102 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
103 |> add_if_present(params, "header", :banner, fn value ->
104 with %Plug.Upload{} <- value,
105 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
112 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
114 with changeset <- User.update_changeset(user, user_params),
115 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
116 {:ok, user} <- User.update_and_set_cache(changeset) do
117 if original_user != user do
118 CommonAPI.update(user)
121 json(conn, AccountView.render("account.json", %{user: user, for: user}))
126 |> json(%{error: "Invalid request"})
130 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131 account = AccountView.render("account.json", %{user: user, for: user})
135 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
136 with %User{} = user <- Repo.get(User, id),
137 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
138 account = AccountView.render("account.json", %{user: user, for: for_user})
144 |> json(%{error: "Can't find user"})
148 @mastodon_api_level "2.5.0"
150 def masto_instance(conn, _params) do
151 instance = Config.get(:instance)
155 title: Keyword.get(instance, :name),
156 description: Keyword.get(instance, :description),
157 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
158 email: Keyword.get(instance, :email),
160 streaming_api: Pleroma.Web.Endpoint.websocket_url()
162 stats: Stats.get_stats(),
163 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
164 max_toot_chars: Keyword.get(instance, :limit)
170 def peers(conn, _params) do
171 json(conn, Stats.get_peers())
174 defp mastodonized_emoji do
175 Pleroma.Emoji.get_all()
176 |> Enum.map(fn {shortcode, relative_url} ->
177 url = to_string(URI.merge(Web.base_url(), relative_url))
180 "shortcode" => shortcode,
182 "visible_in_picker" => true,
188 def custom_emojis(conn, _params) do
189 mastodon_emoji = mastodonized_emoji()
190 json(conn, mastodon_emoji)
193 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
194 last = List.last(activities)
195 first = List.first(activities)
201 {next_url, prev_url} =
205 Pleroma.Web.Endpoint,
208 Map.merge(params, %{max_id: min})
211 Pleroma.Web.Endpoint,
214 Map.merge(params, %{since_id: max})
220 Pleroma.Web.Endpoint,
222 Map.merge(params, %{max_id: min})
225 Pleroma.Web.Endpoint,
227 Map.merge(params, %{since_id: max})
233 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
239 def home_timeline(%{assigns: %{user: user}} = conn, params) do
242 |> Map.put("type", ["Create", "Announce"])
243 |> Map.put("blocking_user", user)
244 |> Map.put("muting_user", user)
245 |> Map.put("user", user)
248 [user.ap_id | user.following]
249 |> ActivityPub.fetch_activities(params)
250 |> ActivityPub.contain_timeline(user)
254 |> add_link_headers(:home_timeline, activities)
255 |> put_view(StatusView)
256 |> render("index.json", %{activities: activities, for: user, as: :activity})
259 def public_timeline(%{assigns: %{user: user}} = conn, params) do
260 local_only = params["local"] in [true, "True", "true", "1"]
264 |> Map.put("type", ["Create", "Announce"])
265 |> Map.put("local_only", local_only)
266 |> Map.put("blocking_user", user)
267 |> Map.put("muting_user", user)
268 |> ActivityPub.fetch_public_activities()
272 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
273 |> put_view(StatusView)
274 |> render("index.json", %{activities: activities, for: user, as: :activity})
277 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
278 with %User{} = user <- Repo.get(User, params["id"]) do
279 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
282 |> add_link_headers(:user_statuses, activities, params["id"])
283 |> put_view(StatusView)
284 |> render("index.json", %{
285 activities: activities,
292 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
294 ActivityPub.fetch_activities_query(
296 Map.merge(params, %{"type" => "Create", visibility: "direct"})
299 activities = Repo.all(query)
302 |> add_link_headers(:dm_timeline, activities)
303 |> put_view(StatusView)
304 |> render("index.json", %{activities: activities, for: user, as: :activity})
307 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
308 with %Activity{} = activity <- Repo.get(Activity, id),
309 true <- ActivityPub.visible_for_user?(activity, user) do
311 |> put_view(StatusView)
312 |> try_render("status.json", %{activity: activity, for: user})
316 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
317 with %Activity{} = activity <- Repo.get(Activity, id),
319 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
320 "blocking_user" => user,
324 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
326 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
327 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
333 activities: grouped_activities[true] || [],
337 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
342 activities: grouped_activities[false] || [],
346 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
353 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
354 when length(media_ids) > 0 do
357 |> Map.put("status", ".")
359 post_status(conn, params)
362 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
365 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
368 case get_req_header(conn, "idempotency-key") do
370 _ -> Ecto.UUID.generate()
374 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
377 |> put_view(StatusView)
378 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
381 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
382 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
388 |> json(%{error: "Can't delete this post"})
392 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
393 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
395 |> put_view(StatusView)
396 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
400 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
401 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
402 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
404 |> put_view(StatusView)
405 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
409 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
411 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
413 |> put_view(StatusView)
414 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
418 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
419 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
420 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
422 |> put_view(StatusView)
423 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
427 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
428 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
430 |> put_view(StatusView)
431 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
435 |> put_resp_content_type("application/json")
436 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
440 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
441 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
443 |> put_view(StatusView)
444 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
448 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
449 with %Activity{} = activity <- Repo.get(Activity, id),
450 %User{} = user <- User.get_by_nickname(user.nickname),
451 true <- ActivityPub.visible_for_user?(activity, user),
452 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
454 |> put_view(StatusView)
455 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
459 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
460 with %Activity{} = activity <- Repo.get(Activity, id),
461 %User{} = user <- User.get_by_nickname(user.nickname),
462 true <- ActivityPub.visible_for_user?(activity, user),
463 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
465 |> put_view(StatusView)
466 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
470 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
471 activity = Activity.get_by_id(id)
473 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
475 |> put_view(StatusView)
476 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
480 |> put_resp_content_type("application/json")
481 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
485 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
486 activity = Activity.get_by_id(id)
488 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
495 def notifications(%{assigns: %{user: user}} = conn, params) do
496 notifications = Notification.for_user(user, params)
500 |> Enum.map(fn x -> render_notification(user, x) end)
504 |> add_link_headers(:notifications, notifications)
508 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
509 with {:ok, notification} <- Notification.get(user, id) do
510 json(conn, render_notification(user, notification))
514 |> put_resp_content_type("application/json")
515 |> send_resp(403, Jason.encode!(%{"error" => reason}))
519 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
520 Notification.clear(user)
524 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
525 with {:ok, _notif} <- Notification.dismiss(user, id) do
530 |> put_resp_content_type("application/json")
531 |> send_resp(403, Jason.encode!(%{"error" => reason}))
535 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
537 q = from(u in User, where: u.id in ^id)
538 targets = Repo.all(q)
541 |> put_view(AccountView)
542 |> render("relationships.json", %{user: user, targets: targets})
545 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
546 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
548 def update_media(%{assigns: %{user: user}} = conn, data) do
549 with %Object{} = object <- Repo.get(Object, data["id"]),
550 true <- Object.authorize_mutation(object, user),
551 true <- is_binary(data["description"]),
552 description <- data["description"] do
553 new_data = %{object.data | "name" => description}
557 |> Object.change(%{data: new_data})
560 attachment_data = Map.put(new_data, "id", object.id)
563 |> put_view(StatusView)
564 |> render("attachment.json", %{attachment: attachment_data})
568 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
569 with {:ok, object} <-
572 actor: User.ap_id(user),
573 description: Map.get(data, "description")
575 attachment_data = Map.put(object.data, "id", object.id)
578 |> put_view(StatusView)
579 |> render("attachment.json", %{attachment: attachment_data})
583 def favourited_by(conn, %{"id" => id}) do
584 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
585 q = from(u in User, where: u.ap_id in ^likes)
589 |> put_view(AccountView)
590 |> render(AccountView, "accounts.json", %{users: users, as: :user})
596 def reblogged_by(conn, %{"id" => id}) do
597 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
598 q = from(u in User, where: u.ap_id in ^announces)
602 |> put_view(AccountView)
603 |> render("accounts.json", %{users: users, as: :user})
609 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
610 local_only = params["local"] in [true, "True", "true", "1"]
613 [params["tag"], params["any"]]
617 |> Enum.map(&String.downcase(&1))
622 |> Enum.map(&String.downcase(&1))
627 |> Enum.map(&String.downcase(&1))
631 |> Map.put("type", "Create")
632 |> Map.put("local_only", local_only)
633 |> Map.put("blocking_user", user)
634 |> Map.put("muting_user", user)
635 |> Map.put("tag", tags)
636 |> Map.put("tag_all", tag_all)
637 |> Map.put("tag_reject", tag_reject)
638 |> ActivityPub.fetch_public_activities()
642 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
643 |> put_view(StatusView)
644 |> render("index.json", %{activities: activities, for: user, as: :activity})
647 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
648 with %User{} = user <- Repo.get(User, id),
649 {:ok, followers} <- User.get_followers(user) do
652 for_user && user.id == for_user.id -> followers
653 user.info.hide_followers -> []
658 |> put_view(AccountView)
659 |> render("accounts.json", %{users: followers, as: :user})
663 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
664 with %User{} = user <- Repo.get(User, id),
665 {:ok, followers} <- User.get_friends(user) do
668 for_user && user.id == for_user.id -> followers
669 user.info.hide_follows -> []
674 |> put_view(AccountView)
675 |> render("accounts.json", %{users: followers, as: :user})
679 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
680 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
682 |> put_view(AccountView)
683 |> render("accounts.json", %{users: follow_requests, as: :user})
687 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
688 with %User{} = follower <- Repo.get(User, id),
689 {:ok, follower} <- User.maybe_follow(follower, followed),
690 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
691 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
693 ActivityPub.accept(%{
694 to: [follower.ap_id],
696 object: follow_activity.data["id"],
700 |> put_view(AccountView)
701 |> render("relationship.json", %{user: followed, target: follower})
705 |> put_resp_content_type("application/json")
706 |> send_resp(403, Jason.encode!(%{"error" => message}))
710 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
711 with %User{} = follower <- Repo.get(User, id),
712 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
713 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
715 ActivityPub.reject(%{
716 to: [follower.ap_id],
718 object: follow_activity.data["id"],
722 |> put_view(AccountView)
723 |> render("relationship.json", %{user: followed, target: follower})
727 |> put_resp_content_type("application/json")
728 |> send_resp(403, Jason.encode!(%{"error" => message}))
732 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
733 with %User{} = followed <- Repo.get(User, id),
734 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
735 {:ok, _activity} <- ActivityPub.follow(follower, followed),
736 {:ok, follower, followed} <-
737 User.wait_and_refresh(
738 Config.get([:activitypub, :follow_handshake_timeout]),
743 |> put_view(AccountView)
744 |> render("relationship.json", %{user: follower, target: followed})
748 |> put_resp_content_type("application/json")
749 |> send_resp(403, Jason.encode!(%{"error" => message}))
753 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
754 with %User{} = followed <- Repo.get_by(User, nickname: uri),
755 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
756 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
758 |> put_view(AccountView)
759 |> render("account.json", %{user: followed, for: follower})
763 |> put_resp_content_type("application/json")
764 |> send_resp(403, Jason.encode!(%{"error" => message}))
768 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
769 with %User{} = followed <- Repo.get(User, id),
770 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
771 {:ok, follower, _} <- User.unfollow(follower, followed) do
773 |> put_view(AccountView)
774 |> render("relationship.json", %{user: follower, target: followed})
778 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
779 with %User{} = muted <- Repo.get(User, id),
780 {:ok, muter} <- User.mute(muter, muted) do
782 |> put_view(AccountView)
783 |> render("relationship.json", %{user: muter, target: muted})
787 |> put_resp_content_type("application/json")
788 |> send_resp(403, Jason.encode!(%{"error" => message}))
792 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
793 with %User{} = muted <- Repo.get(User, id),
794 {:ok, muter} <- User.unmute(muter, muted) do
796 |> put_view(AccountView)
797 |> render("relationship.json", %{user: muter, target: muted})
801 |> put_resp_content_type("application/json")
802 |> send_resp(403, Jason.encode!(%{"error" => message}))
806 def mutes(%{assigns: %{user: user}} = conn, _) do
807 with muted_accounts <- User.muted_users(user) do
808 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
813 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
814 with %User{} = blocked <- Repo.get(User, id),
815 {:ok, blocker} <- User.block(blocker, blocked),
816 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
818 |> put_view(AccountView)
819 |> render("relationship.json", %{user: blocker, target: blocked})
823 |> put_resp_content_type("application/json")
824 |> send_resp(403, Jason.encode!(%{"error" => message}))
828 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
829 with %User{} = blocked <- Repo.get(User, id),
830 {:ok, blocker} <- User.unblock(blocker, blocked),
831 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
833 |> put_view(AccountView)
834 |> render("relationship.json", %{user: blocker, target: blocked})
838 |> put_resp_content_type("application/json")
839 |> send_resp(403, Jason.encode!(%{"error" => message}))
843 def blocks(%{assigns: %{user: user}} = conn, _) do
844 with blocked_accounts <- User.blocked_users(user) do
845 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
850 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
851 json(conn, info.domain_blocks || [])
854 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
855 User.block_domain(blocker, domain)
859 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
860 User.unblock_domain(blocker, domain)
864 def status_search(user, query) do
866 if Regex.match?(~r/https?:/, query) do
867 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
868 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
869 true <- ActivityPub.visible_for_user?(activity, user) do
879 where: fragment("?->>'type' = 'Create'", a.data),
880 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
883 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
888 order_by: [desc: :id]
891 Repo.all(q) ++ fetched
894 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
895 accounts = User.search(query, params["resolve"] == "true", user)
897 statuses = status_search(user, query)
899 tags_path = Web.base_url() <> "/tag/"
905 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
906 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
907 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
910 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
912 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
919 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
920 accounts = User.search(query, params["resolve"] == "true", user)
922 statuses = status_search(user, query)
928 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
929 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
932 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
934 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
941 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
942 accounts = User.search(query, params["resolve"] == "true", user)
944 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
949 def favourites(%{assigns: %{user: user}} = conn, params) do
952 |> Map.put("type", "Create")
953 |> Map.put("favorited_by", user.ap_id)
954 |> Map.put("blocking_user", user)
955 |> ActivityPub.fetch_public_activities()
959 |> add_link_headers(:favourites, activities)
960 |> put_view(StatusView)
961 |> render("index.json", %{activities: activities, for: user, as: :activity})
964 def bookmarks(%{assigns: %{user: user}} = conn, _) do
965 user = Repo.get(User, user.id)
969 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
973 |> put_view(StatusView)
974 |> render("index.json", %{activities: activities, for: user, as: :activity})
977 def get_lists(%{assigns: %{user: user}} = conn, opts) do
978 lists = Pleroma.List.for_user(user, opts)
979 res = ListView.render("lists.json", lists: lists)
983 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
984 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
985 res = ListView.render("list.json", list: list)
991 |> json(%{error: "Record not found"})
995 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
996 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
997 res = ListView.render("lists.json", lists: lists)
1001 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1002 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1003 {:ok, _list} <- Pleroma.List.delete(list) do
1011 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1012 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1013 res = ListView.render("list.json", list: list)
1018 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1020 |> Enum.each(fn account_id ->
1021 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1022 %User{} = followed <- Repo.get(User, account_id) do
1023 Pleroma.List.follow(list, followed)
1030 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1032 |> Enum.each(fn account_id ->
1033 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1034 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1035 Pleroma.List.unfollow(list, followed)
1042 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1043 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1044 {:ok, users} = Pleroma.List.get_following(list) do
1046 |> put_view(AccountView)
1047 |> render("accounts.json", %{users: users, as: :user})
1051 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1052 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1053 {:ok, list} <- Pleroma.List.rename(list, title) do
1054 res = ListView.render("list.json", list: list)
1062 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1063 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1066 |> Map.put("type", "Create")
1067 |> Map.put("blocking_user", user)
1068 |> Map.put("muting_user", user)
1070 # we must filter the following list for the user to avoid leaking statuses the user
1071 # does not actually have permission to see (for more info, peruse security issue #270).
1074 |> Enum.filter(fn x -> x in user.following end)
1075 |> ActivityPub.fetch_activities_bounded(following, params)
1079 |> put_view(StatusView)
1080 |> render("index.json", %{activities: activities, for: user, as: :activity})
1085 |> json(%{error: "Error."})
1089 def index(%{assigns: %{user: user}} = conn, _params) do
1092 |> get_session(:oauth_token)
1095 mastodon_emoji = mastodonized_emoji()
1097 limit = Config.get([:instance, :limit])
1100 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1102 flavour = get_user_flavour(user)
1107 streaming_api_base_url:
1108 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1109 access_token: token,
1111 domain: Pleroma.Web.Endpoint.host(),
1114 unfollow_modal: false,
1117 auto_play_gif: false,
1118 display_sensitive_media: false,
1119 reduce_motion: false,
1120 max_toot_chars: limit
1123 delete_others_notice: present?(user.info.is_moderator),
1124 admin: present?(user.info.is_admin)
1128 default_privacy: user.info.default_scope,
1129 default_sensitive: false
1131 media_attachments: %{
1132 accept_content_types: [
1148 user.info.settings ||
1178 push_subscription: nil,
1180 custom_emojis: mastodon_emoji,
1186 |> put_layout(false)
1187 |> put_view(MastodonView)
1188 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1191 |> redirect(to: "/web/login")
1195 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1196 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1198 with changeset <- Ecto.Changeset.change(user),
1199 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1200 {:ok, _user} <- User.update_and_set_cache(changeset) do
1205 |> put_resp_content_type("application/json")
1206 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1210 @supported_flavours ["glitch", "vanilla"]
1212 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1213 when flavour in @supported_flavours do
1214 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1216 with changeset <- Ecto.Changeset.change(user),
1217 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1218 {:ok, user} <- User.update_and_set_cache(changeset),
1219 flavour <- user.info.flavour do
1224 |> put_resp_content_type("application/json")
1225 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1229 def set_flavour(conn, _params) do
1232 |> json(%{error: "Unsupported flavour"})
1235 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1236 json(conn, get_user_flavour(user))
1239 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1243 defp get_user_flavour(_) do
1247 def login(conn, %{"code" => code}) do
1248 with {:ok, app} <- get_or_make_app(),
1249 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1250 {:ok, token} <- Token.exchange_token(app, auth) do
1252 |> put_session(:oauth_token, token.token)
1253 |> redirect(to: "/web/getting-started")
1257 def login(conn, _) do
1258 with {:ok, app} <- get_or_make_app() do
1263 response_type: "code",
1264 client_id: app.client_id,
1266 scope: Enum.join(app.scopes, " ")
1270 |> redirect(to: path)
1274 defp get_or_make_app() do
1275 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1276 scopes = ["read", "write", "follow", "push"]
1278 with %App{} = app <- Repo.get_by(App, find_attrs) do
1280 if app.scopes == scopes do
1284 |> Ecto.Changeset.change(%{scopes: scopes})
1292 App.register_changeset(
1294 Map.put(find_attrs, :scopes, scopes)
1301 def logout(conn, _) do
1304 |> redirect(to: "/")
1307 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1308 Logger.debug("Unimplemented, returning unmodified relationship")
1310 with %User{} = target <- Repo.get(User, id) do
1312 |> put_view(AccountView)
1313 |> render("relationship.json", %{user: user, target: target})
1317 def empty_array(conn, _) do
1318 Logger.debug("Unimplemented, returning an empty array")
1322 def empty_object(conn, _) do
1323 Logger.debug("Unimplemented, returning an empty object")
1327 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1328 actor = User.get_cached_by_ap_id(activity.data["actor"])
1329 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1330 mastodon_type = Activity.mastodon_notification_type(activity)
1334 type: mastodon_type,
1335 created_at: CommonAPI.Utils.to_masto_date(created_at),
1336 account: AccountView.render("account.json", %{user: actor, for: user})
1339 case mastodon_type do
1343 status: StatusView.render("status.json", %{activity: activity, for: user})
1349 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1355 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1366 def get_filters(%{assigns: %{user: user}} = conn, _) do
1367 filters = Filter.get_filters(user)
1368 res = FilterView.render("filters.json", filters: filters)
1373 %{assigns: %{user: user}} = conn,
1374 %{"phrase" => phrase, "context" => context} = params
1380 hide: Map.get(params, "irreversible", nil),
1381 whole_word: Map.get(params, "boolean", true)
1385 {:ok, response} = Filter.create(query)
1386 res = FilterView.render("filter.json", filter: response)
1390 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1391 filter = Filter.get(filter_id, user)
1392 res = FilterView.render("filter.json", filter: filter)
1397 %{assigns: %{user: user}} = conn,
1398 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1402 filter_id: filter_id,
1405 hide: Map.get(params, "irreversible", nil),
1406 whole_word: Map.get(params, "boolean", true)
1410 {:ok, response} = Filter.update(query)
1411 res = FilterView.render("filter.json", filter: response)
1415 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1418 filter_id: filter_id
1421 {:ok, _} = Filter.delete(query)
1425 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1426 true = Push.enabled()
1427 Subscription.delete_if_exists(user, token)
1428 {:ok, subscription} = Subscription.create(user, token, params)
1429 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1433 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1434 true = Push.enabled()
1435 subscription = Subscription.get(user, token)
1436 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1440 def update_push_subscription(
1441 %{assigns: %{user: user, token: token}} = conn,
1444 true = Push.enabled()
1445 {:ok, subscription} = Subscription.update(user, token, params)
1446 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1450 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1451 true = Push.enabled()
1452 {:ok, _response} = Subscription.delete(user, token)
1456 def errors(conn, _) do
1459 |> json("Something went wrong")
1462 def suggestions(%{assigns: %{user: user}} = conn, _) do
1463 suggestions = Config.get(:suggestions)
1465 if Keyword.get(suggestions, :enabled, false) do
1466 api = Keyword.get(suggestions, :third_party_engine, "")
1467 timeout = Keyword.get(suggestions, :timeout, 5000)
1468 limit = Keyword.get(suggestions, :limit, 23)
1470 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1472 user = user.nickname
1476 |> String.replace("{{host}}", host)
1477 |> String.replace("{{user}}", user)
1479 with {:ok, %{status: 200, body: body}} <-
1485 recv_timeout: timeout,
1489 {:ok, data} <- Jason.decode(body) do
1492 |> Enum.slice(0, limit)
1497 case User.get_or_fetch(x["acct"]) do
1504 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1507 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1513 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1520 def status_card(conn, %{"id" => status_id}) do
1521 with %Activity{} = activity <- Repo.get(Activity, status_id),
1522 true <- ActivityPub.is_public?(activity) do
1526 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1536 def try_render(conn, target, params)
1537 when is_binary(target) do
1538 res = render(conn, target, params)
1543 |> json(%{error: "Can't display this activity"})
1549 def try_render(conn, _, _) do
1552 |> json(%{error: "Can't display this activity"})
1555 defp present?(nil), do: false
1556 defp present?(false), do: false
1557 defp present?(_), do: true