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],
683 actor: followed.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],
705 actor: followed.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}))
1057 streaming_api_base_url:
1058 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1059 access_token: token,
1061 domain: Pleroma.Web.Endpoint.host(),
1064 unfollow_modal: false,
1067 auto_play_gif: false,
1068 display_sensitive_media: false,
1069 reduce_motion: false,
1070 max_toot_chars: limit
1073 delete_others_notice: present?(user.info.is_moderator),
1074 admin: present?(user.info.is_admin)
1078 default_privacy: user.info.default_scope,
1079 default_sensitive: false
1081 media_attachments: %{
1082 accept_content_types: [
1098 user.info.settings ||
1128 push_subscription: nil,
1130 custom_emojis: mastodon_emoji,
1136 |> put_layout(false)
1137 |> put_view(MastodonView)
1138 |> render("index.html", %{initial_state: initial_state})
1141 |> redirect(to: "/web/login")
1145 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1146 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1148 with changeset <- Ecto.Changeset.change(user),
1149 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1150 {:ok, _user} <- User.update_and_set_cache(changeset) do
1155 |> put_resp_content_type("application/json")
1156 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1160 def login(conn, %{"code" => code}) do
1161 with {:ok, app} <- get_or_make_app(),
1162 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1163 {:ok, token} <- Token.exchange_token(app, auth) do
1165 |> put_session(:oauth_token, token.token)
1166 |> redirect(to: "/web/getting-started")
1170 def login(conn, _) do
1171 with {:ok, app} <- get_or_make_app() do
1176 response_type: "code",
1177 client_id: app.client_id,
1183 |> redirect(to: path)
1187 defp get_or_make_app() do
1188 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1190 with %App{} = app <- Repo.get_by(App, find_attrs) do
1194 cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
1200 def logout(conn, _) do
1203 |> redirect(to: "/")
1206 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1207 Logger.debug("Unimplemented, returning unmodified relationship")
1209 with %User{} = target <- Repo.get(User, id) do
1211 |> put_view(AccountView)
1212 |> render("relationship.json", %{user: user, target: target})
1216 def empty_array(conn, _) do
1217 Logger.debug("Unimplemented, returning an empty array")
1221 def empty_object(conn, _) do
1222 Logger.debug("Unimplemented, returning an empty object")
1226 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1227 actor = User.get_cached_by_ap_id(activity.data["actor"])
1228 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1229 mastodon_type = Activity.mastodon_notification_type(activity)
1233 type: mastodon_type,
1234 created_at: CommonAPI.Utils.to_masto_date(created_at),
1235 account: AccountView.render("account.json", %{user: actor, for: user})
1238 case mastodon_type do
1242 status: StatusView.render("status.json", %{activity: activity, for: user})
1248 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1254 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1265 def get_filters(%{assigns: %{user: user}} = conn, _) do
1266 filters = Filter.get_filters(user)
1267 res = FilterView.render("filters.json", filters: filters)
1272 %{assigns: %{user: user}} = conn,
1273 %{"phrase" => phrase, "context" => context} = params
1279 hide: Map.get(params, "irreversible", nil),
1280 whole_word: Map.get(params, "boolean", true)
1284 {:ok, response} = Filter.create(query)
1285 res = FilterView.render("filter.json", filter: response)
1289 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1290 filter = Filter.get(filter_id, user)
1291 res = FilterView.render("filter.json", filter: filter)
1296 %{assigns: %{user: user}} = conn,
1297 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1301 filter_id: filter_id,
1304 hide: Map.get(params, "irreversible", nil),
1305 whole_word: Map.get(params, "boolean", true)
1309 {:ok, response} = Filter.update(query)
1310 res = FilterView.render("filter.json", filter: response)
1314 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1317 filter_id: filter_id
1320 {:ok, _} = Filter.delete(query)
1324 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1325 true = Push.enabled()
1326 Subscription.delete_if_exists(user, token)
1327 {:ok, subscription} = Subscription.create(user, token, params)
1328 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1332 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1333 true = Push.enabled()
1334 subscription = Subscription.get(user, token)
1335 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1339 def update_push_subscription(
1340 %{assigns: %{user: user, token: token}} = conn,
1343 true = Push.enabled()
1344 {:ok, subscription} = Subscription.update(user, token, params)
1345 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1349 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1350 true = Push.enabled()
1351 {:ok, _response} = Subscription.delete(user, token)
1355 def errors(conn, _) do
1358 |> json("Something went wrong")
1361 def suggestions(%{assigns: %{user: user}} = conn, _) do
1362 suggestions = Config.get(:suggestions)
1364 if Keyword.get(suggestions, :enabled, false) do
1365 api = Keyword.get(suggestions, :third_party_engine, "")
1366 timeout = Keyword.get(suggestions, :timeout, 5000)
1367 limit = Keyword.get(suggestions, :limit, 23)
1369 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1371 user = user.nickname
1375 |> String.replace("{{host}}", host)
1376 |> String.replace("{{user}}", user)
1378 with {:ok, %{status: 200, body: body}} <-
1384 recv_timeout: timeout,
1388 {:ok, data} <- Jason.decode(body) do
1391 |> Enum.slice(0, limit)
1396 case User.get_or_fetch(x["acct"]) do
1403 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1406 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1412 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1419 def status_card(conn, %{"id" => status_id}) do
1420 with %Activity{} = activity <- Repo.get(Activity, status_id),
1421 true <- ActivityPub.is_public?(activity) do
1425 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1435 def try_render(conn, target, params)
1436 when is_binary(target) do
1437 res = render(conn, target, params)
1442 |> json(%{error: "Can't display this activity"})
1448 def try_render(conn, _, _) do
1451 |> json(%{error: "Can't display this activity"})
1454 defp present?(nil), do: false
1455 defp present?(false), do: false
1456 defp present?(_), do: true