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 = MastodonAPI.get_notifications(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 followed = User.get_cached_by_id(id)
736 case conn.params["reblogs"] do
737 true -> CommonAPI.show_reblogs(follower, followed)
738 false -> CommonAPI.hide_reblogs(follower, followed)
742 |> put_view(AccountView)
743 |> render("relationship.json", %{user: follower, target: followed})
747 |> put_resp_content_type("application/json")
748 |> send_resp(403, Jason.encode!(%{"error" => message}))
752 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
753 with %User{} = followed <- Repo.get_by(User, nickname: uri),
754 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
756 |> put_view(AccountView)
757 |> render("account.json", %{user: followed, for: follower})
761 |> put_resp_content_type("application/json")
762 |> send_resp(403, Jason.encode!(%{"error" => message}))
766 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
767 with %User{} = followed <- Repo.get(User, id),
768 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
770 |> put_view(AccountView)
771 |> render("relationship.json", %{user: follower, target: followed})
775 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
776 with %User{} = muted <- Repo.get(User, id),
777 {:ok, muter} <- User.mute(muter, muted) do
779 |> put_view(AccountView)
780 |> render("relationship.json", %{user: muter, target: muted})
784 |> put_resp_content_type("application/json")
785 |> send_resp(403, Jason.encode!(%{"error" => message}))
789 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
790 with %User{} = muted <- Repo.get(User, id),
791 {:ok, muter} <- User.unmute(muter, muted) do
793 |> put_view(AccountView)
794 |> render("relationship.json", %{user: muter, target: muted})
798 |> put_resp_content_type("application/json")
799 |> send_resp(403, Jason.encode!(%{"error" => message}))
803 def mutes(%{assigns: %{user: user}} = conn, _) do
804 with muted_accounts <- User.muted_users(user) do
805 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
810 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
811 with %User{} = blocked <- Repo.get(User, id),
812 {:ok, blocker} <- User.block(blocker, blocked),
813 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
815 |> put_view(AccountView)
816 |> render("relationship.json", %{user: blocker, target: blocked})
820 |> put_resp_content_type("application/json")
821 |> send_resp(403, Jason.encode!(%{"error" => message}))
825 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
826 with %User{} = blocked <- Repo.get(User, id),
827 {:ok, blocker} <- User.unblock(blocker, blocked),
828 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
830 |> put_view(AccountView)
831 |> render("relationship.json", %{user: blocker, target: blocked})
835 |> put_resp_content_type("application/json")
836 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 def blocks(%{assigns: %{user: user}} = conn, _) do
841 with blocked_accounts <- User.blocked_users(user) do
842 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
847 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
848 json(conn, info.domain_blocks || [])
851 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
852 User.block_domain(blocker, domain)
856 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
857 User.unblock_domain(blocker, domain)
861 def status_search(user, query) do
863 if Regex.match?(~r/https?:/, query) do
864 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
865 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
866 true <- Visibility.visible_for_user?(activity, user) do
876 where: fragment("?->>'type' = 'Create'", a.data),
877 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
880 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
885 order_by: [desc: :id]
888 Repo.all(q) ++ fetched
891 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
892 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
894 statuses = status_search(user, query)
896 tags_path = Web.base_url() <> "/tag/"
902 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
903 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
904 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
907 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
909 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
916 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
917 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
919 statuses = status_search(user, query)
925 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
926 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
929 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
931 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
938 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
939 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
941 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
946 def favourites(%{assigns: %{user: user}} = conn, params) do
949 |> Map.put("type", "Create")
950 |> Map.put("favorited_by", user.ap_id)
951 |> Map.put("blocking_user", user)
952 |> ActivityPub.fetch_public_activities()
956 |> add_link_headers(:favourites, activities)
957 |> put_view(StatusView)
958 |> render("index.json", %{activities: activities, for: user, as: :activity})
961 def bookmarks(%{assigns: %{user: user}} = conn, _) do
962 user = Repo.get(User, user.id)
966 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
970 |> put_view(StatusView)
971 |> render("index.json", %{activities: activities, for: user, as: :activity})
974 def get_lists(%{assigns: %{user: user}} = conn, opts) do
975 lists = Pleroma.List.for_user(user, opts)
976 res = ListView.render("lists.json", lists: lists)
980 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
981 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
982 res = ListView.render("list.json", list: list)
988 |> json(%{error: "Record not found"})
992 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
993 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
994 res = ListView.render("lists.json", lists: lists)
998 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
999 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1000 {:ok, _list} <- Pleroma.List.delete(list) do
1008 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1009 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1010 res = ListView.render("list.json", list: list)
1015 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1017 |> Enum.each(fn account_id ->
1018 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1019 %User{} = followed <- Repo.get(User, account_id) do
1020 Pleroma.List.follow(list, followed)
1027 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1029 |> Enum.each(fn account_id ->
1030 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1031 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1032 Pleroma.List.unfollow(list, followed)
1039 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1040 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1041 {:ok, users} = Pleroma.List.get_following(list) do
1043 |> put_view(AccountView)
1044 |> render("accounts.json", %{users: users, as: :user})
1048 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1049 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1050 {:ok, list} <- Pleroma.List.rename(list, title) do
1051 res = ListView.render("list.json", list: list)
1059 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1060 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1063 |> Map.put("type", "Create")
1064 |> Map.put("blocking_user", user)
1065 |> Map.put("muting_user", user)
1067 # we must filter the following list for the user to avoid leaking statuses the user
1068 # does not actually have permission to see (for more info, peruse security issue #270).
1071 |> Enum.filter(fn x -> x in user.following end)
1072 |> ActivityPub.fetch_activities_bounded(following, params)
1076 |> put_view(StatusView)
1077 |> render("index.json", %{activities: activities, for: user, as: :activity})
1082 |> json(%{error: "Error."})
1086 def index(%{assigns: %{user: user}} = conn, _params) do
1089 |> get_session(:oauth_token)
1092 mastodon_emoji = mastodonized_emoji()
1094 limit = Config.get([:instance, :limit])
1097 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1099 flavour = get_user_flavour(user)
1104 streaming_api_base_url:
1105 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1106 access_token: token,
1108 domain: Pleroma.Web.Endpoint.host(),
1111 unfollow_modal: false,
1114 auto_play_gif: false,
1115 display_sensitive_media: false,
1116 reduce_motion: false,
1117 max_toot_chars: limit
1120 delete_others_notice: present?(user.info.is_moderator),
1121 admin: present?(user.info.is_admin)
1125 default_privacy: user.info.default_scope,
1126 default_sensitive: false,
1127 allow_content_types: Config.get([:instance, :allowed_post_formats])
1129 media_attachments: %{
1130 accept_content_types: [
1146 user.info.settings ||
1176 push_subscription: nil,
1178 custom_emojis: mastodon_emoji,
1184 |> put_layout(false)
1185 |> put_view(MastodonView)
1186 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1189 |> redirect(to: "/web/login")
1193 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1194 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1196 with changeset <- Ecto.Changeset.change(user),
1197 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1198 {:ok, _user} <- User.update_and_set_cache(changeset) do
1203 |> put_resp_content_type("application/json")
1204 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1208 @supported_flavours ["glitch", "vanilla"]
1210 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1211 when flavour in @supported_flavours do
1212 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1214 with changeset <- Ecto.Changeset.change(user),
1215 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1216 {:ok, user} <- User.update_and_set_cache(changeset),
1217 flavour <- user.info.flavour do
1222 |> put_resp_content_type("application/json")
1223 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1227 def set_flavour(conn, _params) do
1230 |> json(%{error: "Unsupported flavour"})
1233 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1234 json(conn, get_user_flavour(user))
1237 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1241 defp get_user_flavour(_) do
1245 def login(conn, %{"code" => code}) do
1246 with {:ok, app} <- get_or_make_app(),
1247 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1248 {:ok, token} <- Token.exchange_token(app, auth) do
1250 |> put_session(:oauth_token, token.token)
1251 |> redirect(to: "/web/getting-started")
1255 def login(conn, _) do
1256 with {:ok, app} <- get_or_make_app() do
1261 response_type: "code",
1262 client_id: app.client_id,
1264 scope: Enum.join(app.scopes, " ")
1268 |> redirect(to: path)
1272 defp get_or_make_app do
1273 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1274 scopes = ["read", "write", "follow", "push"]
1276 with %App{} = app <- Repo.get_by(App, find_attrs) do
1278 if app.scopes == scopes do
1282 |> Ecto.Changeset.change(%{scopes: scopes})
1290 App.register_changeset(
1292 Map.put(find_attrs, :scopes, scopes)
1299 def logout(conn, _) do
1302 |> redirect(to: "/")
1305 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1306 Logger.debug("Unimplemented, returning unmodified relationship")
1308 with %User{} = target <- Repo.get(User, id) do
1310 |> put_view(AccountView)
1311 |> render("relationship.json", %{user: user, target: target})
1315 def empty_array(conn, _) do
1316 Logger.debug("Unimplemented, returning an empty array")
1320 def empty_object(conn, _) do
1321 Logger.debug("Unimplemented, returning an empty object")
1325 def get_filters(%{assigns: %{user: user}} = conn, _) do
1326 filters = Filter.get_filters(user)
1327 res = FilterView.render("filters.json", filters: filters)
1332 %{assigns: %{user: user}} = conn,
1333 %{"phrase" => phrase, "context" => context} = params
1339 hide: Map.get(params, "irreversible", nil),
1340 whole_word: Map.get(params, "boolean", true)
1344 {:ok, response} = Filter.create(query)
1345 res = FilterView.render("filter.json", filter: response)
1349 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1350 filter = Filter.get(filter_id, user)
1351 res = FilterView.render("filter.json", filter: filter)
1356 %{assigns: %{user: user}} = conn,
1357 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1361 filter_id: filter_id,
1364 hide: Map.get(params, "irreversible", nil),
1365 whole_word: Map.get(params, "boolean", true)
1369 {:ok, response} = Filter.update(query)
1370 res = FilterView.render("filter.json", filter: response)
1374 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1377 filter_id: filter_id
1380 {:ok, _} = Filter.delete(query)
1386 def errors(conn, _) do
1389 |> json("Something went wrong")
1392 def suggestions(%{assigns: %{user: user}} = conn, _) do
1393 suggestions = Config.get(:suggestions)
1395 if Keyword.get(suggestions, :enabled, false) do
1396 api = Keyword.get(suggestions, :third_party_engine, "")
1397 timeout = Keyword.get(suggestions, :timeout, 5000)
1398 limit = Keyword.get(suggestions, :limit, 23)
1400 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1402 user = user.nickname
1406 |> String.replace("{{host}}", host)
1407 |> String.replace("{{user}}", user)
1409 with {:ok, %{status: 200, body: body}} <-
1414 recv_timeout: timeout,
1418 {:ok, data} <- Jason.decode(body) do
1421 |> Enum.slice(0, limit)
1426 case User.get_or_fetch(x["acct"]) do
1433 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1436 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1442 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1449 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1450 with %Activity{} = activity <- Repo.get(Activity, status_id),
1451 true <- Visibility.visible_for_user?(activity, user) do
1455 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1465 def reports(%{assigns: %{user: user}} = conn, params) do
1466 case CommonAPI.report(user, params) do
1469 |> put_view(ReportView)
1470 |> try_render("report.json", %{activity: activity})
1474 |> put_status(:bad_request)
1475 |> json(%{error: err})
1479 def try_render(conn, target, params)
1480 when is_binary(target) do
1481 res = render(conn, target, params)
1486 |> json(%{error: "Can't display this activity"})
1492 def try_render(conn, _, _) do
1495 |> json(%{error: "Can't display this activity"})
1498 defp present?(nil), do: false
1499 defp present?(false), do: false
1500 defp present?(_), do: true