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
195 |> Map.drop(["since_id", "max_id"])
198 last = List.last(activities)
199 first = List.first(activities)
205 {next_url, prev_url} =
209 Pleroma.Web.Endpoint,
212 Map.merge(params, %{max_id: min})
215 Pleroma.Web.Endpoint,
218 Map.merge(params, %{since_id: max})
224 Pleroma.Web.Endpoint,
226 Map.merge(params, %{max_id: min})
229 Pleroma.Web.Endpoint,
231 Map.merge(params, %{since_id: max})
237 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
243 def home_timeline(%{assigns: %{user: user}} = conn, params) do
246 |> Map.put("type", ["Create", "Announce"])
247 |> Map.put("blocking_user", user)
248 |> Map.put("muting_user", user)
249 |> Map.put("user", user)
252 [user.ap_id | user.following]
253 |> ActivityPub.fetch_activities(params)
254 |> ActivityPub.contain_timeline(user)
258 |> add_link_headers(:home_timeline, activities)
259 |> put_view(StatusView)
260 |> render("index.json", %{activities: activities, for: user, as: :activity})
263 def public_timeline(%{assigns: %{user: user}} = conn, params) do
264 local_only = params["local"] in [true, "True", "true", "1"]
268 |> Map.put("type", ["Create", "Announce"])
269 |> Map.put("local_only", local_only)
270 |> Map.put("blocking_user", user)
271 |> Map.put("muting_user", user)
272 |> ActivityPub.fetch_public_activities()
276 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
277 |> put_view(StatusView)
278 |> render("index.json", %{activities: activities, for: user, as: :activity})
281 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
282 with %User{} = user <- Repo.get(User, params["id"]) do
283 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
286 |> add_link_headers(:user_statuses, activities, params["id"])
287 |> put_view(StatusView)
288 |> render("index.json", %{
289 activities: activities,
296 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
299 |> Map.put("type", "Create")
300 |> Map.put("blocking_user", user)
301 |> Map.put("user", user)
302 |> Map.put(:visibility, "direct")
306 |> ActivityPub.fetch_activities_query(params)
310 |> add_link_headers(:dm_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
316 with %Activity{} = activity <- Repo.get(Activity, id),
317 true <- Visibility.visible_for_user?(activity, user) do
319 |> put_view(StatusView)
320 |> try_render("status.json", %{activity: activity, for: user})
324 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
325 with %Activity{} = activity <- Repo.get(Activity, id),
327 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
328 "blocking_user" => user,
332 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
334 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
335 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
341 activities: grouped_activities[true] || [],
345 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
350 activities: grouped_activities[false] || [],
354 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
361 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
362 when length(media_ids) > 0 do
365 |> Map.put("status", ".")
367 post_status(conn, params)
370 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
373 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
376 case get_req_header(conn, "idempotency-key") do
378 _ -> Ecto.UUID.generate()
382 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
385 |> put_view(StatusView)
386 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
389 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
390 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
396 |> json(%{error: "Can't delete this post"})
400 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
401 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
403 |> put_view(StatusView)
404 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
408 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
409 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
410 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
412 |> put_view(StatusView)
413 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
417 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
418 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
419 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
421 |> put_view(StatusView)
422 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
426 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
427 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
428 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
430 |> put_view(StatusView)
431 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
435 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
436 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
438 |> put_view(StatusView)
439 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
443 |> put_resp_content_type("application/json")
444 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
448 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
449 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
451 |> put_view(StatusView)
452 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
456 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
457 with %Activity{} = activity <- Repo.get(Activity, id),
458 %User{} = user <- User.get_by_nickname(user.nickname),
459 true <- Visibility.visible_for_user?(activity, user),
460 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
462 |> put_view(StatusView)
463 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
467 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
468 with %Activity{} = activity <- Repo.get(Activity, id),
469 %User{} = user <- User.get_by_nickname(user.nickname),
470 true <- Visibility.visible_for_user?(activity, user),
471 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 activity = Activity.get_by_id(id)
481 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
483 |> put_view(StatusView)
484 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
488 |> put_resp_content_type("application/json")
489 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
493 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
494 activity = Activity.get_by_id(id)
496 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
498 |> put_view(StatusView)
499 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
503 def notifications(%{assigns: %{user: user}} = conn, params) do
504 notifications = Notification.for_user(user, params)
508 |> Enum.map(fn x -> render_notification(user, x) end)
512 |> add_link_headers(:notifications, notifications)
516 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
517 with {:ok, notification} <- Notification.get(user, id) do
518 json(conn, render_notification(user, notification))
522 |> put_resp_content_type("application/json")
523 |> send_resp(403, Jason.encode!(%{"error" => reason}))
527 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
528 Notification.clear(user)
532 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
533 with {:ok, _notif} <- Notification.dismiss(user, id) do
538 |> put_resp_content_type("application/json")
539 |> send_resp(403, Jason.encode!(%{"error" => reason}))
543 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
545 q = from(u in User, where: u.id in ^id)
546 targets = Repo.all(q)
549 |> put_view(AccountView)
550 |> render("relationships.json", %{user: user, targets: targets})
553 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
554 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
556 def update_media(%{assigns: %{user: user}} = conn, data) do
557 with %Object{} = object <- Repo.get(Object, data["id"]),
558 true <- Object.authorize_mutation(object, user),
559 true <- is_binary(data["description"]),
560 description <- data["description"] do
561 new_data = %{object.data | "name" => description}
565 |> Object.change(%{data: new_data})
568 attachment_data = Map.put(new_data, "id", object.id)
571 |> put_view(StatusView)
572 |> render("attachment.json", %{attachment: attachment_data})
576 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
577 with {:ok, object} <-
580 actor: User.ap_id(user),
581 description: Map.get(data, "description")
583 attachment_data = Map.put(object.data, "id", object.id)
586 |> put_view(StatusView)
587 |> render("attachment.json", %{attachment: attachment_data})
591 def favourited_by(conn, %{"id" => id}) do
592 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
593 q = from(u in User, where: u.ap_id in ^likes)
597 |> put_view(AccountView)
598 |> render(AccountView, "accounts.json", %{users: users, as: :user})
604 def reblogged_by(conn, %{"id" => id}) do
605 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
606 q = from(u in User, where: u.ap_id in ^announces)
610 |> put_view(AccountView)
611 |> render("accounts.json", %{users: users, as: :user})
617 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
618 local_only = params["local"] in [true, "True", "true", "1"]
621 [params["tag"], params["any"]]
625 |> Enum.map(&String.downcase(&1))
630 |> Enum.map(&String.downcase(&1))
635 |> Enum.map(&String.downcase(&1))
639 |> Map.put("type", "Create")
640 |> Map.put("local_only", local_only)
641 |> Map.put("blocking_user", user)
642 |> Map.put("muting_user", user)
643 |> Map.put("tag", tags)
644 |> Map.put("tag_all", tag_all)
645 |> Map.put("tag_reject", tag_reject)
646 |> ActivityPub.fetch_public_activities()
650 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
651 |> put_view(StatusView)
652 |> render("index.json", %{activities: activities, for: user, as: :activity})
655 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
656 with %User{} = user <- Repo.get(User, id),
657 {:ok, followers} <- User.get_followers(user) do
660 for_user && user.id == for_user.id -> followers
661 user.info.hide_followers -> []
666 |> put_view(AccountView)
667 |> render("accounts.json", %{users: followers, as: :user})
671 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
672 with %User{} = user <- Repo.get(User, id),
673 {:ok, followers} <- User.get_friends(user) do
676 for_user && user.id == for_user.id -> followers
677 user.info.hide_follows -> []
682 |> put_view(AccountView)
683 |> render("accounts.json", %{users: followers, as: :user})
687 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
688 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
690 |> put_view(AccountView)
691 |> render("accounts.json", %{users: follow_requests, as: :user})
695 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
696 with %User{} = follower <- Repo.get(User, id),
697 {:ok, follower} <- User.maybe_follow(follower, followed),
698 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
699 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
701 ActivityPub.accept(%{
702 to: [follower.ap_id],
704 object: follow_activity.data["id"],
708 |> put_view(AccountView)
709 |> render("relationship.json", %{user: followed, target: follower})
713 |> put_resp_content_type("application/json")
714 |> send_resp(403, Jason.encode!(%{"error" => message}))
718 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
719 with %User{} = follower <- Repo.get(User, id),
720 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
721 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
723 ActivityPub.reject(%{
724 to: [follower.ap_id],
726 object: follow_activity.data["id"],
730 |> put_view(AccountView)
731 |> render("relationship.json", %{user: followed, target: follower})
735 |> put_resp_content_type("application/json")
736 |> send_resp(403, Jason.encode!(%{"error" => message}))
740 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
741 with %User{} = followed <- Repo.get(User, id),
742 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
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, followed, _} <- CommonAPI.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 <- Visibility.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, resolve: params["resolve"] == "true", for_user: 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, resolve: params["resolve"] == "true", for_user: 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, resolve: params["resolve"] == "true", for_user: 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)
1427 def errors(conn, _) do
1430 |> json("Something went wrong")
1433 def suggestions(%{assigns: %{user: user}} = conn, _) do
1434 suggestions = Config.get(:suggestions)
1436 if Keyword.get(suggestions, :enabled, false) do
1437 api = Keyword.get(suggestions, :third_party_engine, "")
1438 timeout = Keyword.get(suggestions, :timeout, 5000)
1439 limit = Keyword.get(suggestions, :limit, 23)
1441 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1443 user = user.nickname
1447 |> String.replace("{{host}}", host)
1448 |> String.replace("{{user}}", user)
1450 with {:ok, %{status: 200, body: body}} <-
1456 recv_timeout: timeout,
1460 {:ok, data} <- Jason.decode(body) do
1463 |> Enum.slice(0, limit)
1468 case User.get_or_fetch(x["acct"]) do
1475 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1478 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1484 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1491 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1492 with %Activity{} = activity <- Repo.get(Activity, status_id),
1493 true <- Visibility.visible_for_user?(activity, user) do
1497 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1507 def reports(%{assigns: %{user: user}} = conn, params) do
1508 case CommonAPI.report(user, params) do
1511 |> put_view(ReportView)
1512 |> try_render("report.json", %{activity: activity})
1516 |> put_status(:bad_request)
1517 |> json(%{error: err})
1521 def try_render(conn, target, params)
1522 when is_binary(target) do
1523 res = render(conn, target, params)
1528 |> json(%{error: "Can't display this activity"})
1534 def try_render(conn, _, _) do
1537 |> json(%{error: "Can't display this activity"})
1540 defp present?(nil), do: false
1541 defp present?(false), do: false
1542 defp present?(_), do: true