1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
10 alias Pleroma.Notification
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MediaProxy
18 alias Pleroma.Web.Push
19 alias Push.Subscription
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.PushSubscriptionView
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Utils
29 alias Pleroma.Web.OAuth.App
30 alias Pleroma.Web.OAuth.Authorization
31 alias Pleroma.Web.OAuth.Token
36 @httpoison Application.get_env(:pleroma, :httpoison)
37 @local_mastodon_name "Mastodon-Local"
39 action_fallback(:errors)
41 def create_app(conn, params) do
42 with cs <- App.register_changeset(%App{}, params),
43 false <- cs.changes[:client_name] == @local_mastodon_name,
44 {:ok, app} <- Repo.insert(cs) do
46 id: app.id |> to_string,
47 name: app.client_name,
48 client_id: app.client_id,
49 client_secret: app.client_secret,
50 redirect_uri: app.redirect_uris,
63 value_function \\ fn x -> {:ok, x} end
65 if Map.has_key?(params, params_field) do
66 case value_function.(params[params_field]) do
67 {:ok, new_value} -> Map.put(map, map_field, new_value)
75 def update_credentials(%{assigns: %{user: user}} = conn, params) do
80 |> add_if_present(params, "display_name", :name)
81 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
82 |> add_if_present(params, "avatar", :avatar, fn value ->
83 with %Plug.Upload{} <- value,
84 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
93 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
94 |> add_if_present(params, "header", :banner, fn value ->
95 with %Plug.Upload{} <- value,
96 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
103 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
105 with changeset <- User.update_changeset(user, user_params),
106 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
107 {:ok, user} <- User.update_and_set_cache(changeset) do
108 if original_user != user do
109 CommonAPI.update(user)
112 json(conn, AccountView.render("account.json", %{user: user, for: user}))
117 |> json(%{error: "Invalid request"})
121 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
122 account = AccountView.render("account.json", %{user: user, for: user})
126 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
127 with %User{} = user <- Repo.get(User, id),
128 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
129 account = AccountView.render("account.json", %{user: user, for: for_user})
135 |> json(%{error: "Can't find user"})
139 @mastodon_api_level "2.5.0"
141 def masto_instance(conn, _params) do
142 instance = Config.get(:instance)
146 title: Keyword.get(instance, :name),
147 description: Keyword.get(instance, :description),
148 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
149 email: Keyword.get(instance, :email),
151 streaming_api: Pleroma.Web.Endpoint.websocket_url()
153 stats: Stats.get_stats(),
154 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
155 max_toot_chars: Keyword.get(instance, :limit)
161 def peers(conn, _params) do
162 json(conn, Stats.get_peers())
165 defp mastodonized_emoji do
166 Pleroma.Emoji.get_all()
167 |> Enum.map(fn {shortcode, relative_url} ->
168 url = to_string(URI.merge(Web.base_url(), relative_url))
171 "shortcode" => shortcode,
173 "visible_in_picker" => true,
179 def custom_emojis(conn, _params) do
180 mastodon_emoji = mastodonized_emoji()
181 json(conn, mastodon_emoji)
184 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
185 last = List.last(activities)
186 first = List.first(activities)
192 {next_url, prev_url} =
196 Pleroma.Web.Endpoint,
199 Map.merge(params, %{max_id: min})
202 Pleroma.Web.Endpoint,
205 Map.merge(params, %{since_id: max})
211 Pleroma.Web.Endpoint,
213 Map.merge(params, %{max_id: min})
216 Pleroma.Web.Endpoint,
218 Map.merge(params, %{since_id: max})
224 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
230 def home_timeline(%{assigns: %{user: user}} = conn, params) do
233 |> Map.put("type", ["Create", "Announce"])
234 |> Map.put("blocking_user", user)
235 |> Map.put("user", user)
238 [user.ap_id | user.following]
239 |> ActivityPub.fetch_activities(params)
240 |> ActivityPub.contain_timeline(user)
244 |> add_link_headers(:home_timeline, activities)
245 |> put_view(StatusView)
246 |> render("index.json", %{activities: activities, for: user, as: :activity})
249 def public_timeline(%{assigns: %{user: user}} = conn, params) do
250 local_only = params["local"] in [true, "True", "true", "1"]
254 |> Map.put("type", ["Create", "Announce"])
255 |> Map.put("local_only", local_only)
256 |> Map.put("blocking_user", user)
257 |> ActivityPub.fetch_public_activities()
261 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
262 |> put_view(StatusView)
263 |> render("index.json", %{activities: activities, for: user, as: :activity})
266 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
267 with %User{} = user <- Repo.get(User, params["id"]) do
268 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
271 |> add_link_headers(:user_statuses, activities, params["id"])
272 |> put_view(StatusView)
273 |> render("index.json", %{
274 activities: activities,
281 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
283 ActivityPub.fetch_activities_query(
285 Map.merge(params, %{"type" => "Create", visibility: "direct"})
288 activities = Repo.all(query)
291 |> add_link_headers(:dm_timeline, activities)
292 |> put_view(StatusView)
293 |> render("index.json", %{activities: activities, for: user, as: :activity})
296 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
297 with %Activity{} = activity <- Repo.get(Activity, id),
298 true <- ActivityPub.visible_for_user?(activity, user) do
300 |> put_view(StatusView)
301 |> try_render("status.json", %{activity: activity, for: user})
305 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
306 with %Activity{} = activity <- Repo.get(Activity, id),
308 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
309 "blocking_user" => user,
313 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
315 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
316 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
322 activities: grouped_activities[true] || [],
326 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
331 activities: grouped_activities[false] || [],
335 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
342 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
343 when length(media_ids) > 0 do
346 |> Map.put("status", ".")
348 post_status(conn, params)
351 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
354 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
357 case get_req_header(conn, "idempotency-key") do
359 _ -> Ecto.UUID.generate()
363 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
366 |> put_view(StatusView)
367 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
370 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
371 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
377 |> json(%{error: "Can't delete this post"})
381 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
382 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
384 |> put_view(StatusView)
385 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
389 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
390 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
391 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
393 |> put_view(StatusView)
394 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
398 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
399 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
400 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
402 |> put_view(StatusView)
403 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
407 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
408 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
409 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
411 |> put_view(StatusView)
412 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
416 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
417 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
419 |> put_view(StatusView)
420 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
424 |> put_resp_content_type("application/json")
425 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
429 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
430 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
432 |> put_view(StatusView)
433 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
437 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Activity{} = activity <- Repo.get(Activity, id),
439 %User{} = user <- User.get_by_nickname(user.nickname),
440 true <- ActivityPub.visible_for_user?(activity, user),
441 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
443 |> put_view(StatusView)
444 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
448 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
449 with %Activity{} = activity <- Repo.get(Activity, id),
450 %User{} = user <- User.get_by_nickname(user.nickname),
451 true <- ActivityPub.visible_for_user?(activity, user),
452 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
454 |> put_view(StatusView)
455 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
459 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
460 activity = Activity.get_by_id(id)
462 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
464 |> put_view(StatusView)
465 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
469 |> put_resp_content_type("application/json")
470 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
474 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
475 activity = Activity.get_by_id(id)
477 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
479 |> put_view(StatusView)
480 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
484 def notifications(%{assigns: %{user: user}} = conn, params) do
485 notifications = Notification.for_user(user, params)
489 |> Enum.map(fn x -> render_notification(user, x) end)
493 |> add_link_headers(:notifications, notifications)
497 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
498 with {:ok, notification} <- Notification.get(user, id) do
499 json(conn, render_notification(user, notification))
503 |> put_resp_content_type("application/json")
504 |> send_resp(403, Jason.encode!(%{"error" => reason}))
508 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
509 Notification.clear(user)
513 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
514 with {:ok, _notif} <- Notification.dismiss(user, id) do
519 |> put_resp_content_type("application/json")
520 |> send_resp(403, Jason.encode!(%{"error" => reason}))
524 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
526 q = from(u in User, where: u.id in ^id)
527 targets = Repo.all(q)
530 |> put_view(AccountView)
531 |> render("relationships.json", %{user: user, targets: targets})
534 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
535 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
537 def update_media(%{assigns: %{user: user}} = conn, data) do
538 with %Object{} = object <- Repo.get(Object, data["id"]),
539 true <- Object.authorize_mutation(object, user),
540 true <- is_binary(data["description"]),
541 description <- data["description"] do
542 new_data = %{object.data | "name" => description}
546 |> Object.change(%{data: new_data})
549 attachment_data = Map.put(new_data, "id", object.id)
552 |> put_view(StatusView)
553 |> render("attachment.json", %{attachment: attachment_data})
557 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
558 with {:ok, object} <-
561 actor: User.ap_id(user),
562 description: Map.get(data, "description")
564 attachment_data = Map.put(object.data, "id", object.id)
567 |> put_view(StatusView)
568 |> render("attachment.json", %{attachment: attachment_data})
572 def favourited_by(conn, %{"id" => id}) do
573 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
574 q = from(u in User, where: u.ap_id in ^likes)
578 |> put_view(AccountView)
579 |> render(AccountView, "accounts.json", %{users: users, as: :user})
585 def reblogged_by(conn, %{"id" => id}) do
586 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
587 q = from(u in User, where: u.ap_id in ^announces)
591 |> put_view(AccountView)
592 |> render("accounts.json", %{users: users, as: :user})
598 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
599 local_only = params["local"] in [true, "True", "true", "1"]
602 [params["tag"], params["any"]]
606 |> Enum.map(&String.downcase(&1))
611 |> Enum.map(&String.downcase(&1))
616 |> Enum.map(&String.downcase(&1))
620 |> Map.put("type", "Create")
621 |> Map.put("local_only", local_only)
622 |> Map.put("blocking_user", user)
623 |> Map.put("tag", tags)
624 |> Map.put("tag_all", tag_all)
625 |> Map.put("tag_reject", tag_reject)
626 |> ActivityPub.fetch_public_activities()
630 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
631 |> put_view(StatusView)
632 |> render("index.json", %{activities: activities, for: user, as: :activity})
635 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
636 with %User{} = user <- Repo.get(User, id),
637 {:ok, followers} <- User.get_followers(user) do
640 for_user && user.id == for_user.id -> followers
641 user.info.hide_followers -> []
646 |> put_view(AccountView)
647 |> render("accounts.json", %{users: followers, as: :user})
651 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
652 with %User{} = user <- Repo.get(User, id),
653 {:ok, followers} <- User.get_friends(user) do
656 for_user && user.id == for_user.id -> followers
657 user.info.hide_follows -> []
662 |> put_view(AccountView)
663 |> render("accounts.json", %{users: followers, as: :user})
667 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
668 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
670 |> put_view(AccountView)
671 |> render("accounts.json", %{users: follow_requests, as: :user})
675 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
676 with %User{} = follower <- Repo.get(User, id),
677 {:ok, follower} <- User.maybe_follow(follower, followed),
678 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
679 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
681 ActivityPub.accept(%{
682 to: [follower.ap_id],
684 object: follow_activity.data["id"],
688 |> put_view(AccountView)
689 |> render("relationship.json", %{user: followed, target: follower})
693 |> put_resp_content_type("application/json")
694 |> send_resp(403, Jason.encode!(%{"error" => message}))
698 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
699 with %User{} = follower <- Repo.get(User, id),
700 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
701 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
703 ActivityPub.reject(%{
704 to: [follower.ap_id],
706 object: follow_activity.data["id"],
710 |> put_view(AccountView)
711 |> render("relationship.json", %{user: followed, target: follower})
715 |> put_resp_content_type("application/json")
716 |> send_resp(403, Jason.encode!(%{"error" => message}))
720 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
721 with %User{} = followed <- Repo.get(User, id),
722 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
723 {:ok, _activity} <- ActivityPub.follow(follower, followed),
724 {:ok, follower, followed} <-
725 User.wait_and_refresh(
726 Config.get([:activitypub, :follow_handshake_timeout]),
731 |> put_view(AccountView)
732 |> render("relationship.json", %{user: follower, target: followed})
736 |> put_resp_content_type("application/json")
737 |> send_resp(403, Jason.encode!(%{"error" => message}))
741 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
742 with %User{} = followed <- Repo.get_by(User, nickname: uri),
743 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
744 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
746 |> put_view(AccountView)
747 |> render("account.json", %{user: followed, for: follower})
751 |> put_resp_content_type("application/json")
752 |> send_resp(403, Jason.encode!(%{"error" => message}))
756 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
757 with %User{} = followed <- Repo.get(User, id),
758 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
759 {:ok, follower, _} <- User.unfollow(follower, followed) do
761 |> put_view(AccountView)
762 |> render("relationship.json", %{user: follower, target: followed})
766 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
767 with %User{} = blocked <- Repo.get(User, id),
768 {:ok, blocker} <- User.block(blocker, blocked),
769 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
771 |> put_view(AccountView)
772 |> render("relationship.json", %{user: blocker, target: blocked})
776 |> put_resp_content_type("application/json")
777 |> send_resp(403, Jason.encode!(%{"error" => message}))
781 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
782 with %User{} = blocked <- Repo.get(User, id),
783 {:ok, blocker} <- User.unblock(blocker, blocked),
784 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
786 |> put_view(AccountView)
787 |> render("relationship.json", %{user: blocker, target: blocked})
791 |> put_resp_content_type("application/json")
792 |> send_resp(403, Jason.encode!(%{"error" => message}))
796 def blocks(%{assigns: %{user: user}} = conn, _) do
797 with blocked_accounts <- User.blocked_users(user) do
798 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
803 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
804 json(conn, info.domain_blocks || [])
807 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
808 User.block_domain(blocker, domain)
812 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
813 User.unblock_domain(blocker, domain)
817 def status_search(user, query) do
819 if Regex.match?(~r/https?:/, query) do
820 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
821 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
822 true <- ActivityPub.visible_for_user?(activity, user) do
832 where: fragment("?->>'type' = 'Create'", a.data),
833 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
836 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
841 order_by: [desc: :id]
844 Repo.all(q) ++ fetched
847 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
848 accounts = User.search(query, params["resolve"] == "true", user)
850 statuses = status_search(user, query)
852 tags_path = Web.base_url() <> "/tag/"
858 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
859 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
860 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
863 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
865 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
872 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
873 accounts = User.search(query, params["resolve"] == "true", user)
875 statuses = status_search(user, query)
881 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
882 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
885 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
887 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
894 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
895 accounts = User.search(query, params["resolve"] == "true", user)
897 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
902 def favourites(%{assigns: %{user: user}} = conn, params) do
905 |> Map.put("type", "Create")
906 |> Map.put("favorited_by", user.ap_id)
907 |> Map.put("blocking_user", user)
908 |> ActivityPub.fetch_public_activities()
912 |> add_link_headers(:favourites, activities)
913 |> put_view(StatusView)
914 |> render("index.json", %{activities: activities, for: user, as: :activity})
917 def bookmarks(%{assigns: %{user: user}} = conn, _) do
918 user = Repo.get(User, user.id)
922 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
926 |> put_view(StatusView)
927 |> render("index.json", %{activities: activities, for: user, as: :activity})
930 def get_lists(%{assigns: %{user: user}} = conn, opts) do
931 lists = Pleroma.List.for_user(user, opts)
932 res = ListView.render("lists.json", lists: lists)
936 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
937 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
938 res = ListView.render("list.json", list: list)
944 |> json(%{error: "Record not found"})
948 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
949 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
950 res = ListView.render("lists.json", lists: lists)
954 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
955 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
956 {:ok, _list} <- Pleroma.List.delete(list) do
964 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
965 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
966 res = ListView.render("list.json", list: list)
971 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
973 |> Enum.each(fn account_id ->
974 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
975 %User{} = followed <- Repo.get(User, account_id) do
976 Pleroma.List.follow(list, followed)
983 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
985 |> Enum.each(fn account_id ->
986 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
987 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
988 Pleroma.List.unfollow(list, followed)
995 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
996 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
997 {:ok, users} = Pleroma.List.get_following(list) do
999 |> put_view(AccountView)
1000 |> render("accounts.json", %{users: users, as: :user})
1004 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1005 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1006 {:ok, list} <- Pleroma.List.rename(list, title) do
1007 res = ListView.render("list.json", list: list)
1015 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1016 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1019 |> Map.put("type", "Create")
1020 |> Map.put("blocking_user", user)
1022 # we must filter the following list for the user to avoid leaking statuses the user
1023 # does not actually have permission to see (for more info, peruse security issue #270).
1026 |> Enum.filter(fn x -> x in user.following end)
1027 |> ActivityPub.fetch_activities_bounded(following, params)
1031 |> put_view(StatusView)
1032 |> render("index.json", %{activities: activities, for: user, as: :activity})
1037 |> json(%{error: "Error."})
1041 def index(%{assigns: %{user: user}} = conn, _params) do
1044 |> get_session(:oauth_token)
1047 mastodon_emoji = mastodonized_emoji()
1049 limit = Config.get([:instance, :limit])
1052 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1054 flavour = get_user_flavour(user)
1059 streaming_api_base_url:
1060 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1061 access_token: token,
1063 domain: Pleroma.Web.Endpoint.host(),
1066 unfollow_modal: false,
1069 auto_play_gif: false,
1070 display_sensitive_media: false,
1071 reduce_motion: false,
1072 max_toot_chars: limit
1075 delete_others_notice: present?(user.info.is_moderator),
1076 admin: present?(user.info.is_admin)
1080 default_privacy: user.info.default_scope,
1081 default_sensitive: false
1083 media_attachments: %{
1084 accept_content_types: [
1100 user.info.settings ||
1130 push_subscription: nil,
1132 custom_emojis: mastodon_emoji,
1138 |> put_layout(false)
1139 |> put_view(MastodonView)
1140 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1143 |> redirect(to: "/web/login")
1147 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1148 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1150 with changeset <- Ecto.Changeset.change(user),
1151 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1152 {:ok, _user} <- User.update_and_set_cache(changeset) do
1157 |> put_resp_content_type("application/json")
1158 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1162 @supported_flavours ["glitch", "vanilla"]
1164 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1165 when flavour in @supported_flavours do
1166 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1168 with changeset <- Ecto.Changeset.change(user),
1169 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1170 {:ok, user} <- User.update_and_set_cache(changeset),
1171 flavour <- user.info.flavour do
1176 |> put_resp_content_type("application/json")
1177 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1181 def set_flavour(conn, _params) do
1184 |> json(%{error: "Unsupported flavour"})
1187 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1188 json(conn, get_user_flavour(user))
1191 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1195 defp get_user_flavour(_) do
1199 def login(conn, %{"code" => code}) do
1200 with {:ok, app} <- get_or_make_app(),
1201 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1202 {:ok, token} <- Token.exchange_token(app, auth) do
1204 |> put_session(:oauth_token, token.token)
1205 |> redirect(to: "/web/getting-started")
1209 def login(conn, _) do
1210 with {:ok, app} <- get_or_make_app() do
1215 response_type: "code",
1216 client_id: app.client_id,
1222 |> redirect(to: path)
1226 defp get_or_make_app() do
1227 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1229 with %App{} = app <- Repo.get_by(App, find_attrs) do
1233 cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
1239 def logout(conn, _) do
1242 |> redirect(to: "/")
1245 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1246 Logger.debug("Unimplemented, returning unmodified relationship")
1248 with %User{} = target <- Repo.get(User, id) do
1250 |> put_view(AccountView)
1251 |> render("relationship.json", %{user: user, target: target})
1255 def empty_array(conn, _) do
1256 Logger.debug("Unimplemented, returning an empty array")
1260 def empty_object(conn, _) do
1261 Logger.debug("Unimplemented, returning an empty object")
1265 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1266 actor = User.get_cached_by_ap_id(activity.data["actor"])
1267 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1268 mastodon_type = Activity.mastodon_notification_type(activity)
1272 type: mastodon_type,
1273 created_at: CommonAPI.Utils.to_masto_date(created_at),
1274 account: AccountView.render("account.json", %{user: actor, for: user})
1277 case mastodon_type do
1281 status: StatusView.render("status.json", %{activity: activity, for: user})
1287 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1293 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1304 def get_filters(%{assigns: %{user: user}} = conn, _) do
1305 filters = Filter.get_filters(user)
1306 res = FilterView.render("filters.json", filters: filters)
1311 %{assigns: %{user: user}} = conn,
1312 %{"phrase" => phrase, "context" => context} = params
1318 hide: Map.get(params, "irreversible", nil),
1319 whole_word: Map.get(params, "boolean", true)
1323 {:ok, response} = Filter.create(query)
1324 res = FilterView.render("filter.json", filter: response)
1328 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1329 filter = Filter.get(filter_id, user)
1330 res = FilterView.render("filter.json", filter: filter)
1335 %{assigns: %{user: user}} = conn,
1336 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1340 filter_id: filter_id,
1343 hide: Map.get(params, "irreversible", nil),
1344 whole_word: Map.get(params, "boolean", true)
1348 {:ok, response} = Filter.update(query)
1349 res = FilterView.render("filter.json", filter: response)
1353 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1356 filter_id: filter_id
1359 {:ok, _} = Filter.delete(query)
1363 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1364 true = Push.enabled()
1365 Subscription.delete_if_exists(user, token)
1366 {:ok, subscription} = Subscription.create(user, token, params)
1367 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1371 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1372 true = Push.enabled()
1373 subscription = Subscription.get(user, token)
1374 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1378 def update_push_subscription(
1379 %{assigns: %{user: user, token: token}} = conn,
1382 true = Push.enabled()
1383 {:ok, subscription} = Subscription.update(user, token, params)
1384 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1388 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1389 true = Push.enabled()
1390 {:ok, _response} = Subscription.delete(user, token)
1394 def errors(conn, _) do
1397 |> json("Something went wrong")
1400 def suggestions(%{assigns: %{user: user}} = conn, _) do
1401 suggestions = Config.get(:suggestions)
1403 if Keyword.get(suggestions, :enabled, false) do
1404 api = Keyword.get(suggestions, :third_party_engine, "")
1405 timeout = Keyword.get(suggestions, :timeout, 5000)
1406 limit = Keyword.get(suggestions, :limit, 23)
1408 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1410 user = user.nickname
1414 |> String.replace("{{host}}", host)
1415 |> String.replace("{{user}}", user)
1417 with {:ok, %{status: 200, body: body}} <-
1423 recv_timeout: timeout,
1427 {:ok, data} <- Jason.decode(body) do
1430 |> Enum.slice(0, limit)
1435 case User.get_or_fetch(x["acct"]) do
1442 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1445 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1451 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1458 def status_card(conn, %{"id" => status_id}) do
1459 with %Activity{} = activity <- Repo.get(Activity, status_id),
1460 true <- ActivityPub.is_public?(activity) do
1464 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1474 def try_render(conn, target, params)
1475 when is_binary(target) do
1476 res = render(conn, target, params)
1481 |> json(%{error: "Can't display this activity"})
1487 def try_render(conn, _, _) do
1490 |> json(%{error: "Can't display this activity"})
1493 defp present?(nil), do: false
1494 defp present?(false), do: false
1495 defp present?(_), do: true