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",
164 max_toot_chars: Keyword.get(instance, :limit)
170 def peers(conn, _params) do
171 json(conn, Stats.get_peers())
174 defp mastodonized_emoji do
175 Pleroma.Emoji.get_all()
176 |> Enum.map(fn {shortcode, relative_url} ->
177 url = to_string(URI.merge(Web.base_url(), relative_url))
180 "shortcode" => shortcode,
182 "visible_in_picker" => true,
188 def custom_emojis(conn, _params) do
189 mastodon_emoji = mastodonized_emoji()
190 json(conn, mastodon_emoji)
193 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
196 |> Map.drop(["since_id", "max_id"])
199 last = List.last(activities)
200 first = List.first(activities)
206 {next_url, prev_url} =
210 Pleroma.Web.Endpoint,
213 Map.merge(params, %{max_id: min})
216 Pleroma.Web.Endpoint,
219 Map.merge(params, %{since_id: max})
225 Pleroma.Web.Endpoint,
227 Map.merge(params, %{max_id: min})
230 Pleroma.Web.Endpoint,
232 Map.merge(params, %{since_id: max})
238 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
244 def home_timeline(%{assigns: %{user: user}} = conn, params) do
247 |> Map.put("type", ["Create", "Announce"])
248 |> Map.put("blocking_user", user)
249 |> Map.put("muting_user", user)
250 |> Map.put("user", user)
253 [user.ap_id | user.following]
254 |> ActivityPub.fetch_activities(params)
255 |> ActivityPub.contain_timeline(user)
259 |> add_link_headers(:home_timeline, activities)
260 |> put_view(StatusView)
261 |> render("index.json", %{activities: activities, for: user, as: :activity})
264 def public_timeline(%{assigns: %{user: user}} = conn, params) do
265 local_only = params["local"] in [true, "True", "true", "1"]
269 |> Map.put("type", ["Create", "Announce"])
270 |> Map.put("local_only", local_only)
271 |> Map.put("blocking_user", user)
272 |> Map.put("muting_user", user)
273 |> ActivityPub.fetch_public_activities()
277 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
278 |> put_view(StatusView)
279 |> render("index.json", %{activities: activities, for: user, as: :activity})
282 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
283 with %User{} = user <- Repo.get(User, params["id"]) do
284 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
287 |> add_link_headers(:user_statuses, activities, params["id"])
288 |> put_view(StatusView)
289 |> render("index.json", %{
290 activities: activities,
297 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
300 |> Map.put("type", "Create")
301 |> Map.put("blocking_user", user)
302 |> Map.put("user", user)
303 |> Map.put(:visibility, "direct")
307 |> ActivityPub.fetch_activities_query(params)
311 |> add_link_headers(:dm_timeline, activities)
312 |> put_view(StatusView)
313 |> render("index.json", %{activities: activities, for: user, as: :activity})
316 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
317 with %Activity{} = activity <- Repo.get(Activity, id),
318 true <- Visibility.visible_for_user?(activity, user) do
320 |> put_view(StatusView)
321 |> try_render("status.json", %{activity: activity, for: user})
325 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
326 with %Activity{} = activity <- Repo.get(Activity, id),
328 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
329 "blocking_user" => user,
333 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
335 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
336 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
342 activities: grouped_activities[true] || [],
346 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
351 activities: grouped_activities[false] || [],
355 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
362 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
363 when length(media_ids) > 0 do
366 |> Map.put("status", ".")
368 post_status(conn, params)
371 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
374 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
377 case get_req_header(conn, "idempotency-key") do
379 _ -> Ecto.UUID.generate()
383 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
386 |> put_view(StatusView)
387 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
390 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
391 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
397 |> json(%{error: "Can't delete this post"})
401 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
402 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
404 |> put_view(StatusView)
405 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
409 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
411 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
413 |> put_view(StatusView)
414 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
418 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
419 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
420 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
422 |> put_view(StatusView)
423 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
427 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
428 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
429 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
431 |> put_view(StatusView)
432 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
436 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
437 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
439 |> put_view(StatusView)
440 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
444 |> put_resp_content_type("application/json")
445 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
449 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
450 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
452 |> put_view(StatusView)
453 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
457 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
458 with %Activity{} = activity <- Repo.get(Activity, id),
459 %User{} = user <- User.get_by_nickname(user.nickname),
460 true <- Visibility.visible_for_user?(activity, user),
461 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
463 |> put_view(StatusView)
464 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
468 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
469 with %Activity{} = activity <- Repo.get(Activity, id),
470 %User{} = user <- User.get_by_nickname(user.nickname),
471 true <- Visibility.visible_for_user?(activity, user),
472 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
474 |> put_view(StatusView)
475 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
479 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 activity = Activity.get_by_id(id)
482 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
484 |> put_view(StatusView)
485 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
489 |> put_resp_content_type("application/json")
490 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
494 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
495 activity = Activity.get_by_id(id)
497 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
499 |> put_view(StatusView)
500 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
504 def notifications(%{assigns: %{user: user}} = conn, params) do
505 notifications = Notification.for_user(user, params)
508 |> add_link_headers(:notifications, notifications)
509 |> put_view(NotificationView)
510 |> render("index.json", %{notifications: notifications, for: user})
513 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
514 with {:ok, notification} <- Notification.get(user, id) do
516 |> put_view(NotificationView)
517 |> render("show.json", %{notification: notification, for: user})
521 |> put_resp_content_type("application/json")
522 |> send_resp(403, Jason.encode!(%{"error" => reason}))
526 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
527 Notification.clear(user)
531 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
532 with {:ok, _notif} <- Notification.dismiss(user, id) do
537 |> put_resp_content_type("application/json")
538 |> send_resp(403, Jason.encode!(%{"error" => reason}))
542 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
544 q = from(u in User, where: u.id in ^id)
545 targets = Repo.all(q)
548 |> put_view(AccountView)
549 |> render("relationships.json", %{user: user, targets: targets})
552 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
553 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
555 def update_media(%{assigns: %{user: user}} = conn, data) do
556 with %Object{} = object <- Repo.get(Object, data["id"]),
557 true <- Object.authorize_mutation(object, user),
558 true <- is_binary(data["description"]),
559 description <- data["description"] do
560 new_data = %{object.data | "name" => description}
564 |> Object.change(%{data: new_data})
567 attachment_data = Map.put(new_data, "id", object.id)
570 |> put_view(StatusView)
571 |> render("attachment.json", %{attachment: attachment_data})
575 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
576 with {:ok, object} <-
579 actor: User.ap_id(user),
580 description: Map.get(data, "description")
582 attachment_data = Map.put(object.data, "id", object.id)
585 |> put_view(StatusView)
586 |> render("attachment.json", %{attachment: attachment_data})
590 def favourited_by(conn, %{"id" => id}) do
591 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
592 q = from(u in User, where: u.ap_id in ^likes)
596 |> put_view(AccountView)
597 |> render(AccountView, "accounts.json", %{users: users, as: :user})
603 def reblogged_by(conn, %{"id" => id}) do
604 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
605 q = from(u in User, where: u.ap_id in ^announces)
609 |> put_view(AccountView)
610 |> render("accounts.json", %{users: users, as: :user})
616 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
617 local_only = params["local"] in [true, "True", "true", "1"]
620 [params["tag"], params["any"]]
624 |> Enum.map(&String.downcase(&1))
629 |> Enum.map(&String.downcase(&1))
634 |> Enum.map(&String.downcase(&1))
638 |> Map.put("type", "Create")
639 |> Map.put("local_only", local_only)
640 |> Map.put("blocking_user", user)
641 |> Map.put("muting_user", user)
642 |> Map.put("tag", tags)
643 |> Map.put("tag_all", tag_all)
644 |> Map.put("tag_reject", tag_reject)
645 |> ActivityPub.fetch_public_activities()
649 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
650 |> put_view(StatusView)
651 |> render("index.json", %{activities: activities, for: user, as: :activity})
654 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
655 with %User{} = user <- Repo.get(User, id),
656 followers <- MastodonAPI.get_followers(user, params) do
659 for_user && user.id == for_user.id -> followers
660 user.info.hide_followers -> []
665 |> add_link_headers(:followers, followers, user)
666 |> put_view(AccountView)
667 |> render("accounts.json", %{users: followers, as: :user})
671 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
672 with %User{} = user <- Repo.get(User, id),
673 followers <- MastodonAPI.get_friends(user, params) do
676 for_user && user.id == for_user.id -> followers
677 user.info.hide_follows -> []
682 |> add_link_headers(:following, followers, user)
683 |> put_view(AccountView)
684 |> render("accounts.json", %{users: followers, as: :user})
688 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
689 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
691 |> put_view(AccountView)
692 |> render("accounts.json", %{users: follow_requests, as: :user})
696 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
697 with %User{} = follower <- Repo.get(User, id),
698 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
700 |> put_view(AccountView)
701 |> render("relationship.json", %{user: followed, target: follower})
705 |> put_resp_content_type("application/json")
706 |> send_resp(403, Jason.encode!(%{"error" => message}))
710 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
711 with %User{} = follower <- Repo.get(User, id),
712 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
714 |> put_view(AccountView)
715 |> render("relationship.json", %{user: followed, target: follower})
719 |> put_resp_content_type("application/json")
720 |> send_resp(403, Jason.encode!(%{"error" => message}))
724 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
725 with %User{} = followed <- Repo.get(User, id),
726 false <- User.following?(follower, followed),
727 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
729 |> put_view(AccountView)
730 |> render("relationship.json", %{user: follower, target: followed})
733 case conn.params["reblogs"] do
734 true -> CommonAPI.show_reblogs(follower, id)
735 false -> CommonAPI.hide_reblogs(follower, id)
738 followed = User.get_cached_by_id(id)
741 |> put_view(AccountView)
742 |> render("relationship.json", %{user: follower, target: followed})
746 |> put_resp_content_type("application/json")
747 |> send_resp(403, Jason.encode!(%{"error" => message}))
751 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
752 with %User{} = followed <- Repo.get_by(User, nickname: uri),
753 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
755 |> put_view(AccountView)
756 |> render("account.json", %{user: followed, for: follower})
760 |> put_resp_content_type("application/json")
761 |> send_resp(403, Jason.encode!(%{"error" => message}))
765 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
766 with %User{} = followed <- Repo.get(User, id),
767 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
769 |> put_view(AccountView)
770 |> render("relationship.json", %{user: follower, target: followed})
774 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
775 with %User{} = muted <- Repo.get(User, id),
776 {:ok, muter} <- User.mute(muter, muted) do
778 |> put_view(AccountView)
779 |> render("relationship.json", %{user: muter, target: muted})
783 |> put_resp_content_type("application/json")
784 |> send_resp(403, Jason.encode!(%{"error" => message}))
788 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
789 with %User{} = muted <- Repo.get(User, id),
790 {:ok, muter} <- User.unmute(muter, muted) do
792 |> put_view(AccountView)
793 |> render("relationship.json", %{user: muter, target: muted})
797 |> put_resp_content_type("application/json")
798 |> send_resp(403, Jason.encode!(%{"error" => message}))
802 def mutes(%{assigns: %{user: user}} = conn, _) do
803 with muted_accounts <- User.muted_users(user) do
804 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
809 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
810 with %User{} = blocked <- Repo.get(User, id),
811 {:ok, blocker} <- User.block(blocker, blocked),
812 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
814 |> put_view(AccountView)
815 |> render("relationship.json", %{user: blocker, target: blocked})
819 |> put_resp_content_type("application/json")
820 |> send_resp(403, Jason.encode!(%{"error" => message}))
824 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
825 with %User{} = blocked <- Repo.get(User, id),
826 {:ok, blocker} <- User.unblock(blocker, blocked),
827 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
829 |> put_view(AccountView)
830 |> render("relationship.json", %{user: blocker, target: blocked})
834 |> put_resp_content_type("application/json")
835 |> send_resp(403, Jason.encode!(%{"error" => message}))
839 def blocks(%{assigns: %{user: user}} = conn, _) do
840 with blocked_accounts <- User.blocked_users(user) do
841 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
846 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
847 json(conn, info.domain_blocks || [])
850 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
851 User.block_domain(blocker, domain)
855 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
856 User.unblock_domain(blocker, domain)
860 def status_search(user, query) do
862 if Regex.match?(~r/https?:/, query) do
863 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
864 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
865 true <- Visibility.visible_for_user?(activity, user) do
875 where: fragment("?->>'type' = 'Create'", a.data),
876 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
879 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
884 order_by: [desc: :id]
887 Repo.all(q) ++ fetched
890 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
891 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
893 statuses = status_search(user, query)
895 tags_path = Web.base_url() <> "/tag/"
901 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
902 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
903 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
906 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
908 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
915 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
916 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
918 statuses = status_search(user, query)
924 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
925 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
928 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
930 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
937 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
938 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
940 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
945 def favourites(%{assigns: %{user: user}} = conn, params) do
948 |> Map.put("type", "Create")
949 |> Map.put("favorited_by", user.ap_id)
950 |> Map.put("blocking_user", user)
951 |> ActivityPub.fetch_public_activities()
955 |> add_link_headers(:favourites, activities)
956 |> put_view(StatusView)
957 |> render("index.json", %{activities: activities, for: user, as: :activity})
960 def bookmarks(%{assigns: %{user: user}} = conn, _) do
961 user = Repo.get(User, user.id)
965 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
969 |> put_view(StatusView)
970 |> render("index.json", %{activities: activities, for: user, as: :activity})
973 def get_lists(%{assigns: %{user: user}} = conn, opts) do
974 lists = Pleroma.List.for_user(user, opts)
975 res = ListView.render("lists.json", lists: lists)
979 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
980 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
981 res = ListView.render("list.json", list: list)
987 |> json(%{error: "Record not found"})
991 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
992 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
993 res = ListView.render("lists.json", lists: lists)
997 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
998 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
999 {:ok, _list} <- Pleroma.List.delete(list) do
1007 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1008 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1009 res = ListView.render("list.json", list: list)
1014 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1016 |> Enum.each(fn account_id ->
1017 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1018 %User{} = followed <- Repo.get(User, account_id) do
1019 Pleroma.List.follow(list, followed)
1026 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1028 |> Enum.each(fn account_id ->
1029 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1030 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1031 Pleroma.List.unfollow(list, followed)
1038 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1039 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1040 {:ok, users} = Pleroma.List.get_following(list) do
1042 |> put_view(AccountView)
1043 |> render("accounts.json", %{users: users, as: :user})
1047 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1048 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1049 {:ok, list} <- Pleroma.List.rename(list, title) do
1050 res = ListView.render("list.json", list: list)
1058 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1059 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1062 |> Map.put("type", "Create")
1063 |> Map.put("blocking_user", user)
1064 |> Map.put("muting_user", user)
1066 # we must filter the following list for the user to avoid leaking statuses the user
1067 # does not actually have permission to see (for more info, peruse security issue #270).
1070 |> Enum.filter(fn x -> x in user.following end)
1071 |> ActivityPub.fetch_activities_bounded(following, params)
1075 |> put_view(StatusView)
1076 |> render("index.json", %{activities: activities, for: user, as: :activity})
1081 |> json(%{error: "Error."})
1085 def index(%{assigns: %{user: user}} = conn, _params) do
1088 |> get_session(:oauth_token)
1091 mastodon_emoji = mastodonized_emoji()
1093 limit = Config.get([:instance, :limit])
1096 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1098 flavour = get_user_flavour(user)
1103 streaming_api_base_url:
1104 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1105 access_token: token,
1107 domain: Pleroma.Web.Endpoint.host(),
1110 unfollow_modal: false,
1113 auto_play_gif: false,
1114 display_sensitive_media: false,
1115 reduce_motion: false,
1116 max_toot_chars: limit
1119 delete_others_notice: present?(user.info.is_moderator),
1120 admin: present?(user.info.is_admin)
1124 default_privacy: user.info.default_scope,
1125 default_sensitive: false,
1126 allow_content_types: Config.get([:instance, :allowed_post_formats])
1128 media_attachments: %{
1129 accept_content_types: [
1145 user.info.settings ||
1175 push_subscription: nil,
1177 custom_emojis: mastodon_emoji,
1183 |> put_layout(false)
1184 |> put_view(MastodonView)
1185 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1188 |> redirect(to: "/web/login")
1192 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1193 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1195 with changeset <- Ecto.Changeset.change(user),
1196 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1197 {:ok, _user} <- User.update_and_set_cache(changeset) do
1202 |> put_resp_content_type("application/json")
1203 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1207 @supported_flavours ["glitch", "vanilla"]
1209 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1210 when flavour in @supported_flavours do
1211 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1213 with changeset <- Ecto.Changeset.change(user),
1214 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1215 {:ok, user} <- User.update_and_set_cache(changeset),
1216 flavour <- user.info.flavour do
1221 |> put_resp_content_type("application/json")
1222 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1226 def set_flavour(conn, _params) do
1229 |> json(%{error: "Unsupported flavour"})
1232 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1233 json(conn, get_user_flavour(user))
1236 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1240 defp get_user_flavour(_) do
1244 def login(conn, %{"code" => code}) do
1245 with {:ok, app} <- get_or_make_app(),
1246 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1247 {:ok, token} <- Token.exchange_token(app, auth) do
1249 |> put_session(:oauth_token, token.token)
1250 |> redirect(to: "/web/getting-started")
1254 def login(conn, _) do
1255 with {:ok, app} <- get_or_make_app() do
1260 response_type: "code",
1261 client_id: app.client_id,
1263 scope: Enum.join(app.scopes, " ")
1267 |> redirect(to: path)
1271 defp get_or_make_app do
1272 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1273 scopes = ["read", "write", "follow", "push"]
1275 with %App{} = app <- Repo.get_by(App, find_attrs) do
1277 if app.scopes == scopes do
1281 |> Ecto.Changeset.change(%{scopes: scopes})
1289 App.register_changeset(
1291 Map.put(find_attrs, :scopes, scopes)
1298 def logout(conn, _) do
1301 |> redirect(to: "/")
1304 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1305 Logger.debug("Unimplemented, returning unmodified relationship")
1307 with %User{} = target <- Repo.get(User, id) do
1309 |> put_view(AccountView)
1310 |> render("relationship.json", %{user: user, target: target})
1314 def empty_array(conn, _) do
1315 Logger.debug("Unimplemented, returning an empty array")
1319 def empty_object(conn, _) do
1320 Logger.debug("Unimplemented, returning an empty object")
1324 def get_filters(%{assigns: %{user: user}} = conn, _) do
1325 filters = Filter.get_filters(user)
1326 res = FilterView.render("filters.json", filters: filters)
1331 %{assigns: %{user: user}} = conn,
1332 %{"phrase" => phrase, "context" => context} = params
1338 hide: Map.get(params, "irreversible", nil),
1339 whole_word: Map.get(params, "boolean", true)
1343 {:ok, response} = Filter.create(query)
1344 res = FilterView.render("filter.json", filter: response)
1348 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1349 filter = Filter.get(filter_id, user)
1350 res = FilterView.render("filter.json", filter: filter)
1355 %{assigns: %{user: user}} = conn,
1356 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1360 filter_id: filter_id,
1363 hide: Map.get(params, "irreversible", nil),
1364 whole_word: Map.get(params, "boolean", true)
1368 {:ok, response} = Filter.update(query)
1369 res = FilterView.render("filter.json", filter: response)
1373 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1376 filter_id: filter_id
1379 {:ok, _} = Filter.delete(query)
1385 def errors(conn, _) do
1388 |> json("Something went wrong")
1391 def suggestions(%{assigns: %{user: user}} = conn, _) do
1392 suggestions = Config.get(:suggestions)
1394 if Keyword.get(suggestions, :enabled, false) do
1395 api = Keyword.get(suggestions, :third_party_engine, "")
1396 timeout = Keyword.get(suggestions, :timeout, 5000)
1397 limit = Keyword.get(suggestions, :limit, 23)
1399 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1401 user = user.nickname
1405 |> String.replace("{{host}}", host)
1406 |> String.replace("{{user}}", user)
1408 with {:ok, %{status: 200, body: body}} <-
1413 recv_timeout: timeout,
1417 {:ok, data} <- Jason.decode(body) do
1420 |> Enum.slice(0, limit)
1425 case User.get_or_fetch(x["acct"]) do
1432 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1435 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1441 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1448 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1449 with %Activity{} = activity <- Repo.get(Activity, status_id),
1450 true <- Visibility.visible_for_user?(activity, user) do
1454 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1464 def reports(%{assigns: %{user: user}} = conn, params) do
1465 case CommonAPI.report(user, params) do
1468 |> put_view(ReportView)
1469 |> try_render("report.json", %{activity: activity})
1473 |> put_status(:bad_request)
1474 |> json(%{error: err})
1478 def try_render(conn, target, params)
1479 when is_binary(target) do
1480 res = render(conn, target, params)
1485 |> json(%{error: "Can't display this activity"})
1491 def try_render(conn, _, _) do
1494 |> json(%{error: "Can't display this activity"})
1497 defp present?(nil), do: false
1498 defp present?(false), do: false
1499 defp present?(_), do: true