1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Notification
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.FilterView
22 alias Pleroma.Web.MastodonAPI.ListView
23 alias Pleroma.Web.MastodonAPI.MastodonAPI
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.NotificationView
26 alias Pleroma.Web.MastodonAPI.ReportView
27 alias Pleroma.Web.MastodonAPI.StatusView
28 alias Pleroma.Web.MediaProxy
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" => nickname_or_id}) do
136 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_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",
165 registrations: Pleroma.Config.get([:instance, :registrations_open]),
166 # Extra (not present in Mastodon):
167 max_toot_chars: Keyword.get(instance, :limit)
173 def peers(conn, _params) do
174 json(conn, Stats.get_peers())
177 defp mastodonized_emoji do
178 Pleroma.Emoji.get_all()
179 |> Enum.map(fn {shortcode, relative_url} ->
180 url = to_string(URI.merge(Web.base_url(), relative_url))
183 "shortcode" => shortcode,
185 "visible_in_picker" => true,
191 def custom_emojis(conn, _params) do
192 mastodon_emoji = mastodonized_emoji()
193 json(conn, mastodon_emoji)
196 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
199 |> Map.drop(["since_id", "max_id"])
202 last = List.last(activities)
203 first = List.first(activities)
209 {next_url, prev_url} =
213 Pleroma.Web.Endpoint,
216 Map.merge(params, %{max_id: min})
219 Pleroma.Web.Endpoint,
222 Map.merge(params, %{since_id: max})
228 Pleroma.Web.Endpoint,
230 Map.merge(params, %{max_id: min})
233 Pleroma.Web.Endpoint,
235 Map.merge(params, %{since_id: max})
241 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
247 def home_timeline(%{assigns: %{user: user}} = conn, params) do
250 |> Map.put("type", ["Create", "Announce"])
251 |> Map.put("blocking_user", user)
252 |> Map.put("muting_user", user)
253 |> Map.put("user", user)
256 [user.ap_id | user.following]
257 |> ActivityPub.fetch_activities(params)
258 |> ActivityPub.contain_timeline(user)
262 |> add_link_headers(:home_timeline, activities)
263 |> put_view(StatusView)
264 |> render("index.json", %{activities: activities, for: user, as: :activity})
267 def public_timeline(%{assigns: %{user: user}} = conn, params) do
268 local_only = params["local"] in [true, "True", "true", "1"]
272 |> Map.put("type", ["Create", "Announce"])
273 |> Map.put("local_only", local_only)
274 |> Map.put("blocking_user", user)
275 |> Map.put("muting_user", user)
276 |> ActivityPub.fetch_public_activities()
280 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
281 |> put_view(StatusView)
282 |> render("index.json", %{activities: activities, for: user, as: :activity})
285 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
286 with %User{} = user <- Repo.get(User, params["id"]) do
287 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
290 |> add_link_headers(:user_statuses, activities, params["id"])
291 |> put_view(StatusView)
292 |> render("index.json", %{
293 activities: activities,
300 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
303 |> Map.put("type", "Create")
304 |> Map.put("blocking_user", user)
305 |> Map.put("user", user)
306 |> Map.put(:visibility, "direct")
310 |> ActivityPub.fetch_activities_query(params)
314 |> add_link_headers(:dm_timeline, activities)
315 |> put_view(StatusView)
316 |> render("index.json", %{activities: activities, for: user, as: :activity})
319 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
320 with %Activity{} = activity <- Repo.get(Activity, id),
321 true <- Visibility.visible_for_user?(activity, user) do
323 |> put_view(StatusView)
324 |> try_render("status.json", %{activity: activity, for: user})
328 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
329 with %Activity{} = activity <- Repo.get(Activity, id),
331 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
332 "blocking_user" => user,
336 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
338 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
339 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
345 activities: grouped_activities[true] || [],
349 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
354 activities: grouped_activities[false] || [],
358 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
365 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
366 when length(media_ids) > 0 do
369 |> Map.put("status", ".")
371 post_status(conn, params)
374 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
377 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
380 case get_req_header(conn, "idempotency-key") do
382 _ -> Ecto.UUID.generate()
386 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
389 |> put_view(StatusView)
390 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
393 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
394 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
400 |> json(%{error: "Can't delete this post"})
404 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
405 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
407 |> put_view(StatusView)
408 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
412 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
413 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
414 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
416 |> put_view(StatusView)
417 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
421 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
422 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
423 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
425 |> put_view(StatusView)
426 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
430 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
431 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
432 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
434 |> put_view(StatusView)
435 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
439 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
440 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
442 |> put_view(StatusView)
443 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
447 |> put_resp_content_type("application/json")
448 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
452 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
453 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
455 |> put_view(StatusView)
456 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
460 def bookmark_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 <- Visibility.visible_for_user?(activity, user),
464 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
466 |> put_view(StatusView)
467 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
471 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
472 with %Activity{} = activity <- Repo.get(Activity, id),
473 %User{} = user <- User.get_by_nickname(user.nickname),
474 true <- Visibility.visible_for_user?(activity, user),
475 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
477 |> put_view(StatusView)
478 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
482 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
483 activity = Activity.get_by_id(id)
485 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
487 |> put_view(StatusView)
488 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
492 |> put_resp_content_type("application/json")
493 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
497 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
498 activity = Activity.get_by_id(id)
500 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
502 |> put_view(StatusView)
503 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
507 def notifications(%{assigns: %{user: user}} = conn, params) do
508 notifications = MastodonAPI.get_notifications(user, params)
511 |> add_link_headers(:notifications, notifications)
512 |> put_view(NotificationView)
513 |> render("index.json", %{notifications: notifications, for: user})
516 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
517 with {:ok, notification} <- Notification.get(user, id) do
519 |> put_view(NotificationView)
520 |> render("show.json", %{notification: notification, for: user})
524 |> put_resp_content_type("application/json")
525 |> send_resp(403, Jason.encode!(%{"error" => reason}))
529 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
530 Notification.clear(user)
534 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
535 with {:ok, _notif} <- Notification.dismiss(user, id) do
540 |> put_resp_content_type("application/json")
541 |> send_resp(403, Jason.encode!(%{"error" => reason}))
545 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
547 q = from(u in User, where: u.id in ^id)
548 targets = Repo.all(q)
551 |> put_view(AccountView)
552 |> render("relationships.json", %{user: user, targets: targets})
555 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
556 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
558 def update_media(%{assigns: %{user: user}} = conn, data) do
559 with %Object{} = object <- Repo.get(Object, data["id"]),
560 true <- Object.authorize_mutation(object, user),
561 true <- is_binary(data["description"]),
562 description <- data["description"] do
563 new_data = %{object.data | "name" => description}
567 |> Object.change(%{data: new_data})
570 attachment_data = Map.put(new_data, "id", object.id)
573 |> put_view(StatusView)
574 |> render("attachment.json", %{attachment: attachment_data})
578 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
579 with {:ok, object} <-
582 actor: User.ap_id(user),
583 description: Map.get(data, "description")
585 attachment_data = Map.put(object.data, "id", object.id)
588 |> put_view(StatusView)
589 |> render("attachment.json", %{attachment: attachment_data})
593 def favourited_by(conn, %{"id" => id}) do
594 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
595 q = from(u in User, where: u.ap_id in ^likes)
599 |> put_view(AccountView)
600 |> render(AccountView, "accounts.json", %{users: users, as: :user})
606 def reblogged_by(conn, %{"id" => id}) do
607 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
608 q = from(u in User, where: u.ap_id in ^announces)
612 |> put_view(AccountView)
613 |> render("accounts.json", %{users: users, as: :user})
619 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
620 local_only = params["local"] in [true, "True", "true", "1"]
623 [params["tag"], params["any"]]
627 |> Enum.map(&String.downcase(&1))
632 |> Enum.map(&String.downcase(&1))
637 |> Enum.map(&String.downcase(&1))
641 |> Map.put("type", "Create")
642 |> Map.put("local_only", local_only)
643 |> Map.put("blocking_user", user)
644 |> Map.put("muting_user", user)
645 |> Map.put("tag", tags)
646 |> Map.put("tag_all", tag_all)
647 |> Map.put("tag_reject", tag_reject)
648 |> ActivityPub.fetch_public_activities()
652 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
653 |> put_view(StatusView)
654 |> render("index.json", %{activities: activities, for: user, as: :activity})
657 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
658 with %User{} = user <- Repo.get(User, id),
659 followers <- MastodonAPI.get_followers(user, params) do
662 for_user && user.id == for_user.id -> followers
663 user.info.hide_followers -> []
668 |> add_link_headers(:followers, followers, user)
669 |> put_view(AccountView)
670 |> render("accounts.json", %{users: followers, as: :user})
674 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
675 with %User{} = user <- Repo.get(User, id),
676 followers <- MastodonAPI.get_friends(user, params) do
679 for_user && user.id == for_user.id -> followers
680 user.info.hide_follows -> []
685 |> add_link_headers(:following, followers, user)
686 |> put_view(AccountView)
687 |> render("accounts.json", %{users: followers, as: :user})
691 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
692 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
694 |> put_view(AccountView)
695 |> render("accounts.json", %{users: follow_requests, as: :user})
699 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
700 with %User{} = follower <- Repo.get(User, id),
701 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
703 |> put_view(AccountView)
704 |> render("relationship.json", %{user: followed, target: follower})
708 |> put_resp_content_type("application/json")
709 |> send_resp(403, Jason.encode!(%{"error" => message}))
713 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
714 with %User{} = follower <- Repo.get(User, id),
715 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
717 |> put_view(AccountView)
718 |> render("relationship.json", %{user: followed, target: follower})
722 |> put_resp_content_type("application/json")
723 |> send_resp(403, Jason.encode!(%{"error" => message}))
727 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
728 with %User{} = followed <- Repo.get(User, id),
729 false <- User.following?(follower, followed),
730 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
732 |> put_view(AccountView)
733 |> render("relationship.json", %{user: follower, target: followed})
736 followed = User.get_cached_by_id(id)
739 case conn.params["reblogs"] do
740 true -> CommonAPI.show_reblogs(follower, followed)
741 false -> CommonAPI.hide_reblogs(follower, followed)
745 |> put_view(AccountView)
746 |> render("relationship.json", %{user: follower, target: followed})
750 |> put_resp_content_type("application/json")
751 |> send_resp(403, Jason.encode!(%{"error" => message}))
755 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
756 with %User{} = followed <- Repo.get_by(User, nickname: uri),
757 {:ok, follower, followed, _} <- CommonAPI.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, follower} <- CommonAPI.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)
957 ActivityPub.fetch_activities([], params)
961 |> add_link_headers(:favourites, activities)
962 |> put_view(StatusView)
963 |> render("index.json", %{activities: activities, for: user, as: :activity})
966 def bookmarks(%{assigns: %{user: user}} = conn, _) do
967 user = Repo.get(User, user.id)
971 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
975 |> put_view(StatusView)
976 |> render("index.json", %{activities: activities, for: user, as: :activity})
979 def get_lists(%{assigns: %{user: user}} = conn, opts) do
980 lists = Pleroma.List.for_user(user, opts)
981 res = ListView.render("lists.json", lists: lists)
985 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
987 res = ListView.render("list.json", list: list)
993 |> json(%{error: "Record not found"})
997 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
998 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
999 res = ListView.render("lists.json", lists: lists)
1003 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1004 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1005 {:ok, _list} <- Pleroma.List.delete(list) do
1013 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1014 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1015 res = ListView.render("list.json", list: list)
1020 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1022 |> Enum.each(fn account_id ->
1023 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1024 %User{} = followed <- Repo.get(User, account_id) do
1025 Pleroma.List.follow(list, followed)
1032 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1034 |> Enum.each(fn account_id ->
1035 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1036 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1037 Pleroma.List.unfollow(list, followed)
1044 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1045 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1046 {:ok, users} = Pleroma.List.get_following(list) do
1048 |> put_view(AccountView)
1049 |> render("accounts.json", %{users: users, as: :user})
1053 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1054 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1055 {:ok, list} <- Pleroma.List.rename(list, title) do
1056 res = ListView.render("list.json", list: list)
1064 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1065 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1068 |> Map.put("type", "Create")
1069 |> Map.put("blocking_user", user)
1070 |> Map.put("muting_user", user)
1072 # we must filter the following list for the user to avoid leaking statuses the user
1073 # does not actually have permission to see (for more info, peruse security issue #270).
1076 |> Enum.filter(fn x -> x in user.following end)
1077 |> ActivityPub.fetch_activities_bounded(following, params)
1081 |> put_view(StatusView)
1082 |> render("index.json", %{activities: activities, for: user, as: :activity})
1087 |> json(%{error: "Error."})
1091 def index(%{assigns: %{user: user}} = conn, _params) do
1094 |> get_session(:oauth_token)
1097 mastodon_emoji = mastodonized_emoji()
1099 limit = Config.get([:instance, :limit])
1102 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1104 flavour = get_user_flavour(user)
1109 streaming_api_base_url:
1110 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1111 access_token: token,
1113 domain: Pleroma.Web.Endpoint.host(),
1116 unfollow_modal: false,
1119 auto_play_gif: false,
1120 display_sensitive_media: false,
1121 reduce_motion: false,
1122 max_toot_chars: limit
1125 delete_others_notice: present?(user.info.is_moderator),
1126 admin: present?(user.info.is_admin)
1130 default_privacy: user.info.default_scope,
1131 default_sensitive: false,
1132 allow_content_types: Config.get([:instance, :allowed_post_formats])
1134 media_attachments: %{
1135 accept_content_types: [
1151 user.info.settings ||
1181 push_subscription: nil,
1183 custom_emojis: mastodon_emoji,
1189 |> put_layout(false)
1190 |> put_view(MastodonView)
1191 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1194 |> redirect(to: "/web/login")
1198 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1199 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1201 with changeset <- Ecto.Changeset.change(user),
1202 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1203 {:ok, _user} <- User.update_and_set_cache(changeset) do
1208 |> put_resp_content_type("application/json")
1209 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1213 @supported_flavours ["glitch", "vanilla"]
1215 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1216 when flavour in @supported_flavours do
1217 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1219 with changeset <- Ecto.Changeset.change(user),
1220 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1221 {:ok, user} <- User.update_and_set_cache(changeset),
1222 flavour <- user.info.flavour do
1227 |> put_resp_content_type("application/json")
1228 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1232 def set_flavour(conn, _params) do
1235 |> json(%{error: "Unsupported flavour"})
1238 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1239 json(conn, get_user_flavour(user))
1242 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1246 defp get_user_flavour(_) do
1250 def login(conn, %{"code" => code}) do
1251 with {:ok, app} <- get_or_make_app(),
1252 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1253 {:ok, token} <- Token.exchange_token(app, auth) do
1255 |> put_session(:oauth_token, token.token)
1256 |> redirect(to: "/web/getting-started")
1260 def login(conn, _) do
1261 with {:ok, app} <- get_or_make_app() do
1266 response_type: "code",
1267 client_id: app.client_id,
1269 scope: Enum.join(app.scopes, " ")
1273 |> redirect(to: path)
1277 defp get_or_make_app do
1278 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1279 scopes = ["read", "write", "follow", "push"]
1281 with %App{} = app <- Repo.get_by(App, find_attrs) do
1283 if app.scopes == scopes do
1287 |> Ecto.Changeset.change(%{scopes: scopes})
1295 App.register_changeset(
1297 Map.put(find_attrs, :scopes, scopes)
1304 def logout(conn, _) do
1307 |> redirect(to: "/")
1310 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1311 Logger.debug("Unimplemented, returning unmodified relationship")
1313 with %User{} = target <- Repo.get(User, id) do
1315 |> put_view(AccountView)
1316 |> render("relationship.json", %{user: user, target: target})
1320 def empty_array(conn, _) do
1321 Logger.debug("Unimplemented, returning an empty array")
1325 def empty_object(conn, _) do
1326 Logger.debug("Unimplemented, returning an empty object")
1330 def get_filters(%{assigns: %{user: user}} = conn, _) do
1331 filters = Filter.get_filters(user)
1332 res = FilterView.render("filters.json", filters: filters)
1337 %{assigns: %{user: user}} = conn,
1338 %{"phrase" => phrase, "context" => context} = params
1344 hide: Map.get(params, "irreversible", nil),
1345 whole_word: Map.get(params, "boolean", true)
1349 {:ok, response} = Filter.create(query)
1350 res = FilterView.render("filter.json", filter: response)
1354 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1355 filter = Filter.get(filter_id, user)
1356 res = FilterView.render("filter.json", filter: filter)
1361 %{assigns: %{user: user}} = conn,
1362 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1366 filter_id: filter_id,
1369 hide: Map.get(params, "irreversible", nil),
1370 whole_word: Map.get(params, "boolean", true)
1374 {:ok, response} = Filter.update(query)
1375 res = FilterView.render("filter.json", filter: response)
1379 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1382 filter_id: filter_id
1385 {:ok, _} = Filter.delete(query)
1391 def errors(conn, _) do
1394 |> json("Something went wrong")
1397 def suggestions(%{assigns: %{user: user}} = conn, _) do
1398 suggestions = Config.get(:suggestions)
1400 if Keyword.get(suggestions, :enabled, false) do
1401 api = Keyword.get(suggestions, :third_party_engine, "")
1402 timeout = Keyword.get(suggestions, :timeout, 5000)
1403 limit = Keyword.get(suggestions, :limit, 23)
1405 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1407 user = user.nickname
1411 |> String.replace("{{host}}", host)
1412 |> String.replace("{{user}}", user)
1414 with {:ok, %{status: 200, body: body}} <-
1419 recv_timeout: timeout,
1423 {:ok, data} <- Jason.decode(body) do
1426 |> Enum.slice(0, limit)
1431 case User.get_or_fetch(x["acct"]) do
1438 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1441 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1447 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1454 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1455 with %Activity{} = activity <- Repo.get(Activity, status_id),
1456 true <- Visibility.visible_for_user?(activity, user) do
1460 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1470 def reports(%{assigns: %{user: user}} = conn, params) do
1471 case CommonAPI.report(user, params) do
1474 |> put_view(ReportView)
1475 |> try_render("report.json", %{activity: activity})
1479 |> put_status(:bad_request)
1480 |> json(%{error: err})
1484 def try_render(conn, target, params)
1485 when is_binary(target) do
1486 res = render(conn, target, params)
1491 |> json(%{error: "Can't display this activity"})
1497 def try_render(conn, _, _) do
1500 |> json(%{error: "Can't display this activity"})
1503 defp present?(nil), do: false
1504 defp present?(false), do: false
1505 defp present?(_), do: true