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.MastodonAPI.ReportView
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Utils
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
56 id: app.id |> to_string,
57 name: app.client_name,
58 client_id: app.client_id,
59 client_secret: app.client_secret,
60 redirect_uri: app.redirect_uris,
73 value_function \\ fn x -> {:ok, x} end
75 if Map.has_key?(params, params_field) do
76 case value_function.(params[params_field]) do
77 {:ok, new_value} -> Map.put(map, map_field, new_value)
85 def update_credentials(%{assigns: %{user: user}} = conn, params) do
90 |> add_if_present(params, "display_name", :name)
91 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
92 |> add_if_present(params, "avatar", :avatar, fn value ->
93 with %Plug.Upload{} <- value,
94 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
103 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
104 |> add_if_present(params, "header", :banner, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
113 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
115 with changeset <- User.update_changeset(user, user_params),
116 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
117 {:ok, user} <- User.update_and_set_cache(changeset) do
118 if original_user != user do
119 CommonAPI.update(user)
122 json(conn, AccountView.render("account.json", %{user: user, for: user}))
127 |> json(%{error: "Invalid request"})
131 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
132 account = AccountView.render("account.json", %{user: user, for: user})
136 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
137 with %User{} = user <- Repo.get(User, id),
138 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
139 account = AccountView.render("account.json", %{user: user, for: for_user})
145 |> json(%{error: "Can't find user"})
149 @mastodon_api_level "2.5.0"
151 def masto_instance(conn, _params) do
152 instance = Config.get(:instance)
156 title: Keyword.get(instance, :name),
157 description: Keyword.get(instance, :description),
158 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
159 email: Keyword.get(instance, :email),
161 streaming_api: Pleroma.Web.Endpoint.websocket_url()
163 stats: Stats.get_stats(),
164 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
165 max_toot_chars: Keyword.get(instance, :limit)
171 def peers(conn, _params) do
172 json(conn, Stats.get_peers())
175 defp mastodonized_emoji do
176 Pleroma.Emoji.get_all()
177 |> Enum.map(fn {shortcode, relative_url} ->
178 url = to_string(URI.merge(Web.base_url(), relative_url))
181 "shortcode" => shortcode,
183 "visible_in_picker" => true,
189 def custom_emojis(conn, _params) do
190 mastodon_emoji = mastodonized_emoji()
191 json(conn, mastodon_emoji)
194 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
195 last = List.last(activities)
196 first = List.first(activities)
202 {next_url, prev_url} =
206 Pleroma.Web.Endpoint,
209 Map.merge(params, %{max_id: min})
212 Pleroma.Web.Endpoint,
215 Map.merge(params, %{since_id: max})
221 Pleroma.Web.Endpoint,
223 Map.merge(params, %{max_id: min})
226 Pleroma.Web.Endpoint,
228 Map.merge(params, %{since_id: max})
234 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
240 def home_timeline(%{assigns: %{user: user}} = conn, params) do
243 |> Map.put("type", ["Create", "Announce"])
244 |> Map.put("blocking_user", user)
245 |> Map.put("muting_user", user)
246 |> Map.put("user", user)
249 [user.ap_id | user.following]
250 |> ActivityPub.fetch_activities(params)
251 |> ActivityPub.contain_timeline(user)
255 |> add_link_headers(:home_timeline, activities)
256 |> put_view(StatusView)
257 |> render("index.json", %{activities: activities, for: user, as: :activity})
260 def public_timeline(%{assigns: %{user: user}} = conn, params) do
261 local_only = params["local"] in [true, "True", "true", "1"]
265 |> Map.put("type", ["Create", "Announce"])
266 |> Map.put("local_only", local_only)
267 |> Map.put("blocking_user", user)
268 |> Map.put("muting_user", user)
269 |> ActivityPub.fetch_public_activities()
273 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
274 |> put_view(StatusView)
275 |> render("index.json", %{activities: activities, for: user, as: :activity})
278 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
279 with %User{} = user <- Repo.get(User, params["id"]) do
280 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
283 |> add_link_headers(:user_statuses, activities, params["id"])
284 |> put_view(StatusView)
285 |> render("index.json", %{
286 activities: activities,
293 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
295 ActivityPub.fetch_activities_query(
297 Map.merge(params, %{"type" => "Create", visibility: "direct"})
300 activities = Repo.all(query)
303 |> add_link_headers(:dm_timeline, activities)
304 |> put_view(StatusView)
305 |> render("index.json", %{activities: activities, for: user, as: :activity})
308 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
309 with %Activity{} = activity <- Repo.get(Activity, id),
310 true <- ActivityPub.visible_for_user?(activity, user) do
312 |> put_view(StatusView)
313 |> try_render("status.json", %{activity: activity, for: user})
317 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
318 with %Activity{} = activity <- Repo.get(Activity, id),
320 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
321 "blocking_user" => user,
325 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
327 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
328 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
334 activities: grouped_activities[true] || [],
338 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
343 activities: grouped_activities[false] || [],
347 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
354 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
355 when length(media_ids) > 0 do
358 |> Map.put("status", ".")
360 post_status(conn, params)
363 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
366 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
369 case get_req_header(conn, "idempotency-key") do
371 _ -> Ecto.UUID.generate()
375 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
378 |> put_view(StatusView)
379 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
382 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
383 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
389 |> json(%{error: "Can't delete this post"})
393 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
394 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
396 |> put_view(StatusView)
397 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
401 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
402 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
403 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
405 |> put_view(StatusView)
406 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
410 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
411 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
412 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
414 |> put_view(StatusView)
415 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
419 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
420 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
421 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
423 |> put_view(StatusView)
424 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
428 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
429 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
431 |> put_view(StatusView)
432 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
436 |> put_resp_content_type("application/json")
437 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
441 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
442 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
444 |> put_view(StatusView)
445 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
449 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
450 with %Activity{} = activity <- Repo.get(Activity, id),
451 %User{} = user <- User.get_by_nickname(user.nickname),
452 true <- ActivityPub.visible_for_user?(activity, user),
453 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
455 |> put_view(StatusView)
456 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
460 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
461 with %Activity{} = activity <- Repo.get(Activity, id),
462 %User{} = user <- User.get_by_nickname(user.nickname),
463 true <- ActivityPub.visible_for_user?(activity, user),
464 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
466 |> put_view(StatusView)
467 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
471 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
472 activity = Activity.get_by_id(id)
474 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
476 |> put_view(StatusView)
477 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
481 |> put_resp_content_type("application/json")
482 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
486 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
487 activity = Activity.get_by_id(id)
489 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
491 |> put_view(StatusView)
492 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
496 def notifications(%{assigns: %{user: user}} = conn, params) do
497 notifications = Notification.for_user(user, params)
501 |> Enum.map(fn x -> render_notification(user, x) end)
505 |> add_link_headers(:notifications, notifications)
509 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
510 with {:ok, notification} <- Notification.get(user, id) do
511 json(conn, render_notification(user, notification))
515 |> put_resp_content_type("application/json")
516 |> send_resp(403, Jason.encode!(%{"error" => reason}))
520 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
521 Notification.clear(user)
525 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
526 with {:ok, _notif} <- Notification.dismiss(user, id) do
531 |> put_resp_content_type("application/json")
532 |> send_resp(403, Jason.encode!(%{"error" => reason}))
536 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
538 q = from(u in User, where: u.id in ^id)
539 targets = Repo.all(q)
542 |> put_view(AccountView)
543 |> render("relationships.json", %{user: user, targets: targets})
546 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
547 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
549 def update_media(%{assigns: %{user: user}} = conn, data) do
550 with %Object{} = object <- Repo.get(Object, data["id"]),
551 true <- Object.authorize_mutation(object, user),
552 true <- is_binary(data["description"]),
553 description <- data["description"] do
554 new_data = %{object.data | "name" => description}
558 |> Object.change(%{data: new_data})
561 attachment_data = Map.put(new_data, "id", object.id)
564 |> put_view(StatusView)
565 |> render("attachment.json", %{attachment: attachment_data})
569 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
570 with {:ok, object} <-
573 actor: User.ap_id(user),
574 description: Map.get(data, "description")
576 attachment_data = Map.put(object.data, "id", object.id)
579 |> put_view(StatusView)
580 |> render("attachment.json", %{attachment: attachment_data})
584 def favourited_by(conn, %{"id" => id}) do
585 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
586 q = from(u in User, where: u.ap_id in ^likes)
590 |> put_view(AccountView)
591 |> render(AccountView, "accounts.json", %{users: users, as: :user})
597 def reblogged_by(conn, %{"id" => id}) do
598 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
599 q = from(u in User, where: u.ap_id in ^announces)
603 |> put_view(AccountView)
604 |> render("accounts.json", %{users: users, as: :user})
610 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
611 local_only = params["local"] in [true, "True", "true", "1"]
614 [params["tag"], params["any"]]
618 |> Enum.map(&String.downcase(&1))
623 |> Enum.map(&String.downcase(&1))
628 |> Enum.map(&String.downcase(&1))
632 |> Map.put("type", "Create")
633 |> Map.put("local_only", local_only)
634 |> Map.put("blocking_user", user)
635 |> Map.put("muting_user", user)
636 |> Map.put("tag", tags)
637 |> Map.put("tag_all", tag_all)
638 |> Map.put("tag_reject", tag_reject)
639 |> ActivityPub.fetch_public_activities()
643 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
644 |> put_view(StatusView)
645 |> render("index.json", %{activities: activities, for: user, as: :activity})
648 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
649 with %User{} = user <- Repo.get(User, id),
650 {:ok, followers} <- User.get_followers(user) do
653 for_user && user.id == for_user.id -> followers
654 user.info.hide_followers -> []
659 |> put_view(AccountView)
660 |> render("accounts.json", %{users: followers, as: :user})
664 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
665 with %User{} = user <- Repo.get(User, id),
666 {:ok, followers} <- User.get_friends(user) do
669 for_user && user.id == for_user.id -> followers
670 user.info.hide_follows -> []
675 |> put_view(AccountView)
676 |> render("accounts.json", %{users: followers, as: :user})
680 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
681 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
683 |> put_view(AccountView)
684 |> render("accounts.json", %{users: follow_requests, as: :user})
688 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
689 with %User{} = follower <- Repo.get(User, id),
690 {:ok, follower} <- User.maybe_follow(follower, followed),
691 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
692 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
694 ActivityPub.accept(%{
695 to: [follower.ap_id],
697 object: follow_activity.data["id"],
701 |> put_view(AccountView)
702 |> render("relationship.json", %{user: followed, target: follower})
706 |> put_resp_content_type("application/json")
707 |> send_resp(403, Jason.encode!(%{"error" => message}))
711 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
712 with %User{} = follower <- Repo.get(User, id),
713 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
714 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
716 ActivityPub.reject(%{
717 to: [follower.ap_id],
719 object: follow_activity.data["id"],
723 |> put_view(AccountView)
724 |> render("relationship.json", %{user: followed, target: follower})
728 |> put_resp_content_type("application/json")
729 |> send_resp(403, Jason.encode!(%{"error" => message}))
733 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
734 with %User{} = followed <- Repo.get(User, id),
735 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
736 {:ok, _activity} <- ActivityPub.follow(follower, followed),
737 {:ok, follower, followed} <-
738 User.wait_and_refresh(
739 Config.get([:activitypub, :follow_handshake_timeout]),
744 |> put_view(AccountView)
745 |> render("relationship.json", %{user: follower, target: followed})
749 |> put_resp_content_type("application/json")
750 |> send_resp(403, Jason.encode!(%{"error" => message}))
754 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
755 with %User{} = followed <- Repo.get_by(User, nickname: uri),
756 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
757 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
759 |> put_view(AccountView)
760 |> render("account.json", %{user: followed, for: follower})
764 |> put_resp_content_type("application/json")
765 |> send_resp(403, Jason.encode!(%{"error" => message}))
769 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
770 with %User{} = followed <- Repo.get(User, id),
771 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
772 {:ok, follower, _} <- User.unfollow(follower, followed) do
774 |> put_view(AccountView)
775 |> render("relationship.json", %{user: follower, target: followed})
779 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
780 with %User{} = muted <- Repo.get(User, id),
781 {:ok, muter} <- User.mute(muter, muted) do
783 |> put_view(AccountView)
784 |> render("relationship.json", %{user: muter, target: muted})
788 |> put_resp_content_type("application/json")
789 |> send_resp(403, Jason.encode!(%{"error" => message}))
793 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
794 with %User{} = muted <- Repo.get(User, id),
795 {:ok, muter} <- User.unmute(muter, muted) do
797 |> put_view(AccountView)
798 |> render("relationship.json", %{user: muter, target: muted})
802 |> put_resp_content_type("application/json")
803 |> send_resp(403, Jason.encode!(%{"error" => message}))
807 def mutes(%{assigns: %{user: user}} = conn, _) do
808 with muted_accounts <- User.muted_users(user) do
809 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
814 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
815 with %User{} = blocked <- Repo.get(User, id),
816 {:ok, blocker} <- User.block(blocker, blocked),
817 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
819 |> put_view(AccountView)
820 |> render("relationship.json", %{user: blocker, target: blocked})
824 |> put_resp_content_type("application/json")
825 |> send_resp(403, Jason.encode!(%{"error" => message}))
829 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
830 with %User{} = blocked <- Repo.get(User, id),
831 {:ok, blocker} <- User.unblock(blocker, blocked),
832 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
834 |> put_view(AccountView)
835 |> render("relationship.json", %{user: blocker, target: blocked})
839 |> put_resp_content_type("application/json")
840 |> send_resp(403, Jason.encode!(%{"error" => message}))
844 def blocks(%{assigns: %{user: user}} = conn, _) do
845 with blocked_accounts <- User.blocked_users(user) do
846 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
851 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
852 json(conn, info.domain_blocks || [])
855 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
856 User.block_domain(blocker, domain)
860 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
861 User.unblock_domain(blocker, domain)
865 def status_search(user, query) do
867 if Regex.match?(~r/https?:/, query) do
868 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
869 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
870 true <- ActivityPub.visible_for_user?(activity, user) do
880 where: fragment("?->>'type' = 'Create'", a.data),
881 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
884 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
889 order_by: [desc: :id]
892 Repo.all(q) ++ fetched
895 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
896 accounts = User.search(query, params["resolve"] == "true", user)
898 statuses = status_search(user, query)
900 tags_path = Web.base_url() <> "/tag/"
906 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
907 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
908 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
911 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
913 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
920 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
921 accounts = User.search(query, params["resolve"] == "true", user)
923 statuses = status_search(user, query)
929 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
930 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
933 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
935 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
942 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
943 accounts = User.search(query, params["resolve"] == "true", user)
945 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
950 def favourites(%{assigns: %{user: user}} = conn, params) do
953 |> Map.put("type", "Create")
954 |> Map.put("favorited_by", user.ap_id)
955 |> Map.put("blocking_user", user)
956 |> ActivityPub.fetch_public_activities()
960 |> add_link_headers(:favourites, activities)
961 |> put_view(StatusView)
962 |> render("index.json", %{activities: activities, for: user, as: :activity})
965 def bookmarks(%{assigns: %{user: user}} = conn, _) do
966 user = Repo.get(User, user.id)
970 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
974 |> put_view(StatusView)
975 |> render("index.json", %{activities: activities, for: user, as: :activity})
978 def get_lists(%{assigns: %{user: user}} = conn, opts) do
979 lists = Pleroma.List.for_user(user, opts)
980 res = ListView.render("lists.json", lists: lists)
984 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
985 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
986 res = ListView.render("list.json", list: list)
992 |> json(%{error: "Record not found"})
996 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
997 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
998 res = ListView.render("lists.json", lists: lists)
1002 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1003 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1004 {:ok, _list} <- Pleroma.List.delete(list) do
1012 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1013 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1014 res = ListView.render("list.json", list: list)
1019 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1021 |> Enum.each(fn account_id ->
1022 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1023 %User{} = followed <- Repo.get(User, account_id) do
1024 Pleroma.List.follow(list, followed)
1031 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1033 |> Enum.each(fn account_id ->
1034 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1035 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1036 Pleroma.List.unfollow(list, followed)
1043 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1044 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1045 {:ok, users} = Pleroma.List.get_following(list) do
1047 |> put_view(AccountView)
1048 |> render("accounts.json", %{users: users, as: :user})
1052 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1053 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1054 {:ok, list} <- Pleroma.List.rename(list, title) do
1055 res = ListView.render("list.json", list: list)
1063 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1064 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1067 |> Map.put("type", "Create")
1068 |> Map.put("blocking_user", user)
1069 |> Map.put("muting_user", user)
1071 # we must filter the following list for the user to avoid leaking statuses the user
1072 # does not actually have permission to see (for more info, peruse security issue #270).
1075 |> Enum.filter(fn x -> x in user.following end)
1076 |> ActivityPub.fetch_activities_bounded(following, params)
1080 |> put_view(StatusView)
1081 |> render("index.json", %{activities: activities, for: user, as: :activity})
1086 |> json(%{error: "Error."})
1090 def index(%{assigns: %{user: user}} = conn, _params) do
1093 |> get_session(:oauth_token)
1096 mastodon_emoji = mastodonized_emoji()
1098 limit = Config.get([:instance, :limit])
1101 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1103 flavour = get_user_flavour(user)
1108 streaming_api_base_url:
1109 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1110 access_token: token,
1112 domain: Pleroma.Web.Endpoint.host(),
1115 unfollow_modal: false,
1118 auto_play_gif: false,
1119 display_sensitive_media: false,
1120 reduce_motion: false,
1121 max_toot_chars: limit
1124 delete_others_notice: present?(user.info.is_moderator),
1125 admin: present?(user.info.is_admin)
1129 default_privacy: user.info.default_scope,
1130 default_sensitive: false
1132 media_attachments: %{
1133 accept_content_types: [
1149 user.info.settings ||
1179 push_subscription: nil,
1181 custom_emojis: mastodon_emoji,
1187 |> put_layout(false)
1188 |> put_view(MastodonView)
1189 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1192 |> redirect(to: "/web/login")
1196 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1197 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1199 with changeset <- Ecto.Changeset.change(user),
1200 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1201 {:ok, _user} <- User.update_and_set_cache(changeset) do
1206 |> put_resp_content_type("application/json")
1207 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1211 @supported_flavours ["glitch", "vanilla"]
1213 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1214 when flavour in @supported_flavours do
1215 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1217 with changeset <- Ecto.Changeset.change(user),
1218 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1219 {:ok, user} <- User.update_and_set_cache(changeset),
1220 flavour <- user.info.flavour do
1225 |> put_resp_content_type("application/json")
1226 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1230 def set_flavour(conn, _params) do
1233 |> json(%{error: "Unsupported flavour"})
1236 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1237 json(conn, get_user_flavour(user))
1240 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1244 defp get_user_flavour(_) do
1248 def login(conn, %{"code" => code}) do
1249 with {:ok, app} <- get_or_make_app(),
1250 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1251 {:ok, token} <- Token.exchange_token(app, auth) do
1253 |> put_session(:oauth_token, token.token)
1254 |> redirect(to: "/web/getting-started")
1258 def login(conn, _) do
1259 with {:ok, app} <- get_or_make_app() do
1264 response_type: "code",
1265 client_id: app.client_id,
1267 scope: Enum.join(app.scopes, " ")
1271 |> redirect(to: path)
1275 defp get_or_make_app() do
1276 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1277 scopes = ["read", "write", "follow", "push"]
1279 with %App{} = app <- Repo.get_by(App, find_attrs) do
1281 if app.scopes == scopes do
1285 |> Ecto.Changeset.change(%{scopes: scopes})
1293 App.register_changeset(
1295 Map.put(find_attrs, :scopes, scopes)
1302 def logout(conn, _) do
1305 |> redirect(to: "/")
1308 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1309 Logger.debug("Unimplemented, returning unmodified relationship")
1311 with %User{} = target <- Repo.get(User, id) do
1313 |> put_view(AccountView)
1314 |> render("relationship.json", %{user: user, target: target})
1318 def empty_array(conn, _) do
1319 Logger.debug("Unimplemented, returning an empty array")
1323 def empty_object(conn, _) do
1324 Logger.debug("Unimplemented, returning an empty object")
1328 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1329 actor = User.get_cached_by_ap_id(activity.data["actor"])
1330 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1331 mastodon_type = Activity.mastodon_notification_type(activity)
1335 type: mastodon_type,
1336 created_at: CommonAPI.Utils.to_masto_date(created_at),
1337 account: AccountView.render("account.json", %{user: actor, for: user})
1340 case mastodon_type do
1344 status: StatusView.render("status.json", %{activity: activity, for: user})
1350 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1356 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1367 def get_filters(%{assigns: %{user: user}} = conn, _) do
1368 filters = Filter.get_filters(user)
1369 res = FilterView.render("filters.json", filters: filters)
1374 %{assigns: %{user: user}} = conn,
1375 %{"phrase" => phrase, "context" => context} = params
1381 hide: Map.get(params, "irreversible", nil),
1382 whole_word: Map.get(params, "boolean", true)
1386 {:ok, response} = Filter.create(query)
1387 res = FilterView.render("filter.json", filter: response)
1391 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1392 filter = Filter.get(filter_id, user)
1393 res = FilterView.render("filter.json", filter: filter)
1398 %{assigns: %{user: user}} = conn,
1399 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1403 filter_id: filter_id,
1406 hide: Map.get(params, "irreversible", nil),
1407 whole_word: Map.get(params, "boolean", true)
1411 {:ok, response} = Filter.update(query)
1412 res = FilterView.render("filter.json", filter: response)
1416 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1419 filter_id: filter_id
1422 {:ok, _} = Filter.delete(query)
1426 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1427 true = Push.enabled()
1428 Subscription.delete_if_exists(user, token)
1429 {:ok, subscription} = Subscription.create(user, token, params)
1430 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1434 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1435 true = Push.enabled()
1436 subscription = Subscription.get(user, token)
1437 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1441 def update_push_subscription(
1442 %{assigns: %{user: user, token: token}} = conn,
1445 true = Push.enabled()
1446 {:ok, subscription} = Subscription.update(user, token, params)
1447 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1451 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1452 true = Push.enabled()
1453 {:ok, _response} = Subscription.delete(user, token)
1457 def errors(conn, _) do
1460 |> json("Something went wrong")
1463 def suggestions(%{assigns: %{user: user}} = conn, _) do
1464 suggestions = Config.get(:suggestions)
1466 if Keyword.get(suggestions, :enabled, false) do
1467 api = Keyword.get(suggestions, :third_party_engine, "")
1468 timeout = Keyword.get(suggestions, :timeout, 5000)
1469 limit = Keyword.get(suggestions, :limit, 23)
1471 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1473 user = user.nickname
1477 |> String.replace("{{host}}", host)
1478 |> String.replace("{{user}}", user)
1480 with {:ok, %{status: 200, body: body}} <-
1486 recv_timeout: timeout,
1490 {:ok, data} <- Jason.decode(body) do
1493 |> Enum.slice(0, limit)
1498 case User.get_or_fetch(x["acct"]) do
1505 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1508 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1514 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1521 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1522 with %Activity{} = activity <- Repo.get(Activity, status_id),
1523 true <- ActivityPub.visible_for_user?(activity, user) do
1527 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1537 def reports(%{assigns: %{user: user}} = conn, params) do
1538 case CommonAPI.report(user, params) do
1541 |> put_view(ReportView)
1542 |> try_render("report.json", %{activity: activity})
1546 |> put_status(:bad_request)
1547 |> json(%{error: err})
1551 def try_render(conn, target, params)
1552 when is_binary(target) do
1553 res = render(conn, target, params)
1558 |> json(%{error: "Can't display this activity"})
1564 def try_render(conn, _, _) do
1567 |> json(%{error: "Can't display this activity"})
1570 defp present?(nil), do: false
1571 defp present?(false), do: false
1572 defp present?(_), do: true