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 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
728 |> put_view(AccountView)
729 |> render("relationship.json", %{user: follower, target: followed})
733 |> put_resp_content_type("application/json")
734 |> send_resp(403, Jason.encode!(%{"error" => message}))
738 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
739 with %User{} = followed <- Repo.get_by(User, nickname: uri),
740 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
742 |> put_view(AccountView)
743 |> render("account.json", %{user: followed, for: follower})
747 |> put_resp_content_type("application/json")
748 |> send_resp(403, Jason.encode!(%{"error" => message}))
752 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
753 with %User{} = followed <- Repo.get(User, id),
754 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
756 |> put_view(AccountView)
757 |> render("relationship.json", %{user: follower, target: followed})
761 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
762 with %User{} = muted <- Repo.get(User, id),
763 {:ok, muter} <- User.mute(muter, muted) do
765 |> put_view(AccountView)
766 |> render("relationship.json", %{user: muter, target: muted})
770 |> put_resp_content_type("application/json")
771 |> send_resp(403, Jason.encode!(%{"error" => message}))
775 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
776 with %User{} = muted <- Repo.get(User, id),
777 {:ok, muter} <- User.unmute(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 mutes(%{assigns: %{user: user}} = conn, _) do
790 with muted_accounts <- User.muted_users(user) do
791 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
796 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
797 with %User{} = blocked <- Repo.get(User, id),
798 {:ok, blocker} <- User.block(blocker, blocked),
799 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
801 |> put_view(AccountView)
802 |> render("relationship.json", %{user: blocker, target: blocked})
806 |> put_resp_content_type("application/json")
807 |> send_resp(403, Jason.encode!(%{"error" => message}))
811 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
812 with %User{} = blocked <- Repo.get(User, id),
813 {:ok, blocker} <- User.unblock(blocker, blocked),
814 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
816 |> put_view(AccountView)
817 |> render("relationship.json", %{user: blocker, target: blocked})
821 |> put_resp_content_type("application/json")
822 |> send_resp(403, Jason.encode!(%{"error" => message}))
826 def blocks(%{assigns: %{user: user}} = conn, _) do
827 with blocked_accounts <- User.blocked_users(user) do
828 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
833 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
834 json(conn, info.domain_blocks || [])
837 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
838 User.block_domain(blocker, domain)
842 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
843 User.unblock_domain(blocker, domain)
847 def status_search(user, query) do
849 if Regex.match?(~r/https?:/, query) do
850 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
851 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
852 true <- Visibility.visible_for_user?(activity, user) do
862 where: fragment("?->>'type' = 'Create'", a.data),
863 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
866 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
871 order_by: [desc: :id]
874 Repo.all(q) ++ fetched
877 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
878 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
880 statuses = status_search(user, query)
882 tags_path = Web.base_url() <> "/tag/"
888 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
889 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
890 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
893 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
895 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
902 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
903 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
905 statuses = status_search(user, query)
911 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
912 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
915 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
917 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
924 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
925 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
927 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
932 def favourites(%{assigns: %{user: user}} = conn, params) do
935 |> Map.put("type", "Create")
936 |> Map.put("favorited_by", user.ap_id)
937 |> Map.put("blocking_user", user)
938 |> ActivityPub.fetch_public_activities()
942 |> add_link_headers(:favourites, activities)
943 |> put_view(StatusView)
944 |> render("index.json", %{activities: activities, for: user, as: :activity})
947 def bookmarks(%{assigns: %{user: user}} = conn, _) do
948 user = Repo.get(User, user.id)
952 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
956 |> put_view(StatusView)
957 |> render("index.json", %{activities: activities, for: user, as: :activity})
960 def get_lists(%{assigns: %{user: user}} = conn, opts) do
961 lists = Pleroma.List.for_user(user, opts)
962 res = ListView.render("lists.json", lists: lists)
966 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
967 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
968 res = ListView.render("list.json", list: list)
974 |> json(%{error: "Record not found"})
978 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
979 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
980 res = ListView.render("lists.json", lists: lists)
984 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
985 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
986 {:ok, _list} <- Pleroma.List.delete(list) do
994 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
995 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
996 res = ListView.render("list.json", list: list)
1001 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1003 |> Enum.each(fn account_id ->
1004 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1005 %User{} = followed <- Repo.get(User, account_id) do
1006 Pleroma.List.follow(list, followed)
1013 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1015 |> Enum.each(fn account_id ->
1016 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1017 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1018 Pleroma.List.unfollow(list, followed)
1025 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1026 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1027 {:ok, users} = Pleroma.List.get_following(list) do
1029 |> put_view(AccountView)
1030 |> render("accounts.json", %{users: users, as: :user})
1034 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1035 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1036 {:ok, list} <- Pleroma.List.rename(list, title) do
1037 res = ListView.render("list.json", list: list)
1045 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1046 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1049 |> Map.put("type", "Create")
1050 |> Map.put("blocking_user", user)
1051 |> Map.put("muting_user", user)
1053 # we must filter the following list for the user to avoid leaking statuses the user
1054 # does not actually have permission to see (for more info, peruse security issue #270).
1057 |> Enum.filter(fn x -> x in user.following end)
1058 |> ActivityPub.fetch_activities_bounded(following, params)
1062 |> put_view(StatusView)
1063 |> render("index.json", %{activities: activities, for: user, as: :activity})
1068 |> json(%{error: "Error."})
1072 def index(%{assigns: %{user: user}} = conn, _params) do
1075 |> get_session(:oauth_token)
1078 mastodon_emoji = mastodonized_emoji()
1080 limit = Config.get([:instance, :limit])
1083 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1085 flavour = get_user_flavour(user)
1090 streaming_api_base_url:
1091 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1092 access_token: token,
1094 domain: Pleroma.Web.Endpoint.host(),
1097 unfollow_modal: false,
1100 auto_play_gif: false,
1101 display_sensitive_media: false,
1102 reduce_motion: false,
1103 max_toot_chars: limit
1106 delete_others_notice: present?(user.info.is_moderator),
1107 admin: present?(user.info.is_admin)
1111 default_privacy: user.info.default_scope,
1112 default_sensitive: false,
1113 allow_content_types: Config.get([:instance, :allowed_post_formats])
1115 media_attachments: %{
1116 accept_content_types: [
1132 user.info.settings ||
1162 push_subscription: nil,
1164 custom_emojis: mastodon_emoji,
1170 |> put_layout(false)
1171 |> put_view(MastodonView)
1172 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1175 |> redirect(to: "/web/login")
1179 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1180 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1182 with changeset <- Ecto.Changeset.change(user),
1183 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1184 {:ok, _user} <- User.update_and_set_cache(changeset) do
1189 |> put_resp_content_type("application/json")
1190 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1194 @supported_flavours ["glitch", "vanilla"]
1196 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1197 when flavour in @supported_flavours do
1198 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1200 with changeset <- Ecto.Changeset.change(user),
1201 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1202 {:ok, user} <- User.update_and_set_cache(changeset),
1203 flavour <- user.info.flavour do
1208 |> put_resp_content_type("application/json")
1209 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1213 def set_flavour(conn, _params) do
1216 |> json(%{error: "Unsupported flavour"})
1219 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1220 json(conn, get_user_flavour(user))
1223 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1227 defp get_user_flavour(_) do
1231 def login(conn, %{"code" => code}) do
1232 with {:ok, app} <- get_or_make_app(),
1233 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1234 {:ok, token} <- Token.exchange_token(app, auth) do
1236 |> put_session(:oauth_token, token.token)
1237 |> redirect(to: "/web/getting-started")
1241 def login(conn, _) do
1242 with {:ok, app} <- get_or_make_app() do
1247 response_type: "code",
1248 client_id: app.client_id,
1250 scope: Enum.join(app.scopes, " ")
1254 |> redirect(to: path)
1258 defp get_or_make_app do
1259 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1260 scopes = ["read", "write", "follow", "push"]
1262 with %App{} = app <- Repo.get_by(App, find_attrs) do
1264 if app.scopes == scopes do
1268 |> Ecto.Changeset.change(%{scopes: scopes})
1276 App.register_changeset(
1278 Map.put(find_attrs, :scopes, scopes)
1285 def logout(conn, _) do
1288 |> redirect(to: "/")
1291 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1292 Logger.debug("Unimplemented, returning unmodified relationship")
1294 with %User{} = target <- Repo.get(User, id) do
1296 |> put_view(AccountView)
1297 |> render("relationship.json", %{user: user, target: target})
1301 def empty_array(conn, _) do
1302 Logger.debug("Unimplemented, returning an empty array")
1306 def empty_object(conn, _) do
1307 Logger.debug("Unimplemented, returning an empty object")
1311 def get_filters(%{assigns: %{user: user}} = conn, _) do
1312 filters = Filter.get_filters(user)
1313 res = FilterView.render("filters.json", filters: filters)
1318 %{assigns: %{user: user}} = conn,
1319 %{"phrase" => phrase, "context" => context} = params
1325 hide: Map.get(params, "irreversible", nil),
1326 whole_word: Map.get(params, "boolean", true)
1330 {:ok, response} = Filter.create(query)
1331 res = FilterView.render("filter.json", filter: response)
1335 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1336 filter = Filter.get(filter_id, user)
1337 res = FilterView.render("filter.json", filter: filter)
1342 %{assigns: %{user: user}} = conn,
1343 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1347 filter_id: filter_id,
1350 hide: Map.get(params, "irreversible", nil),
1351 whole_word: Map.get(params, "boolean", true)
1355 {:ok, response} = Filter.update(query)
1356 res = FilterView.render("filter.json", filter: response)
1360 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1363 filter_id: filter_id
1366 {:ok, _} = Filter.delete(query)
1372 def errors(conn, _) do
1375 |> json("Something went wrong")
1378 def suggestions(%{assigns: %{user: user}} = conn, _) do
1379 suggestions = Config.get(:suggestions)
1381 if Keyword.get(suggestions, :enabled, false) do
1382 api = Keyword.get(suggestions, :third_party_engine, "")
1383 timeout = Keyword.get(suggestions, :timeout, 5000)
1384 limit = Keyword.get(suggestions, :limit, 23)
1386 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1388 user = user.nickname
1392 |> String.replace("{{host}}", host)
1393 |> String.replace("{{user}}", user)
1395 with {:ok, %{status: 200, body: body}} <-
1400 recv_timeout: timeout,
1404 {:ok, data} <- Jason.decode(body) do
1407 |> Enum.slice(0, limit)
1412 case User.get_or_fetch(x["acct"]) do
1419 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1422 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1428 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1435 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1436 with %Activity{} = activity <- Repo.get(Activity, status_id),
1437 true <- Visibility.visible_for_user?(activity, user) do
1441 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1451 def reports(%{assigns: %{user: user}} = conn, params) do
1452 case CommonAPI.report(user, params) do
1455 |> put_view(ReportView)
1456 |> try_render("report.json", %{activity: activity})
1460 |> put_status(:bad_request)
1461 |> json(%{error: err})
1465 def try_render(conn, target, params)
1466 when is_binary(target) do
1467 res = render(conn, target, params)
1472 |> json(%{error: "Can't display this activity"})
1478 def try_render(conn, _, _) do
1481 |> json(%{error: "Can't display this activity"})
1484 defp present?(nil), do: false
1485 defp present?(false), do: false
1486 defp present?(_), do: true