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 notifications(%{assigns: %{user: user}} = conn, params) do
460 notifications = Notification.for_user(user, params)
464 |> Enum.map(fn x -> render_notification(user, x) end)
468 |> add_link_headers(:notifications, notifications)
472 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
473 with {:ok, notification} <- Notification.get(user, id) do
474 json(conn, render_notification(user, notification))
478 |> put_resp_content_type("application/json")
479 |> send_resp(403, Jason.encode!(%{"error" => reason}))
483 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
484 Notification.clear(user)
488 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
489 with {:ok, _notif} <- Notification.dismiss(user, id) do
494 |> put_resp_content_type("application/json")
495 |> send_resp(403, Jason.encode!(%{"error" => reason}))
499 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
501 q = from(u in User, where: u.id in ^id)
502 targets = Repo.all(q)
505 |> put_view(AccountView)
506 |> render("relationships.json", %{user: user, targets: targets})
509 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
510 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
512 def update_media(%{assigns: %{user: user}} = conn, data) do
513 with %Object{} = object <- Repo.get(Object, data["id"]),
514 true <- Object.authorize_mutation(object, user),
515 true <- is_binary(data["description"]),
516 description <- data["description"] do
517 new_data = %{object.data | "name" => description}
521 |> Object.change(%{data: new_data})
524 attachment_data = Map.put(new_data, "id", object.id)
527 |> put_view(StatusView)
528 |> render("attachment.json", %{attachment: attachment_data})
532 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
533 with {:ok, object} <-
536 actor: User.ap_id(user),
537 description: Map.get(data, "description")
539 attachment_data = Map.put(object.data, "id", object.id)
542 |> put_view(StatusView)
543 |> render("attachment.json", %{attachment: attachment_data})
547 def favourited_by(conn, %{"id" => id}) do
548 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
549 q = from(u in User, where: u.ap_id in ^likes)
553 |> put_view(AccountView)
554 |> render(AccountView, "accounts.json", %{users: users, as: :user})
560 def reblogged_by(conn, %{"id" => id}) do
561 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
562 q = from(u in User, where: u.ap_id in ^announces)
566 |> put_view(AccountView)
567 |> render("accounts.json", %{users: users, as: :user})
573 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
574 local_only = params["local"] in [true, "True", "true", "1"]
577 [params["tag"], params["any"]]
581 |> Enum.map(&String.downcase(&1))
586 |> Enum.map(&String.downcase(&1))
591 |> Enum.map(&String.downcase(&1))
595 |> Map.put("type", "Create")
596 |> Map.put("local_only", local_only)
597 |> Map.put("blocking_user", user)
598 |> Map.put("tag", tags)
599 |> Map.put("tag_all", tag_all)
600 |> Map.put("tag_reject", tag_reject)
601 |> ActivityPub.fetch_public_activities()
605 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
606 |> put_view(StatusView)
607 |> render("index.json", %{activities: activities, for: user, as: :activity})
610 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
611 with %User{} = user <- Repo.get(User, id),
612 {:ok, followers} <- User.get_followers(user) do
615 for_user && user.id == for_user.id -> followers
616 user.info.hide_followers -> []
621 |> put_view(AccountView)
622 |> render("accounts.json", %{users: followers, as: :user})
626 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
627 with %User{} = user <- Repo.get(User, id),
628 {:ok, followers} <- User.get_friends(user) do
631 for_user && user.id == for_user.id -> followers
632 user.info.hide_follows -> []
637 |> put_view(AccountView)
638 |> render("accounts.json", %{users: followers, as: :user})
642 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
643 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
645 |> put_view(AccountView)
646 |> render("accounts.json", %{users: follow_requests, as: :user})
650 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
651 with %User{} = follower <- Repo.get(User, id),
652 {:ok, follower} <- User.maybe_follow(follower, followed),
653 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
654 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
656 ActivityPub.accept(%{
657 to: [follower.ap_id],
658 actor: followed.ap_id,
659 object: follow_activity.data["id"],
663 |> put_view(AccountView)
664 |> render("relationship.json", %{user: followed, target: follower})
668 |> put_resp_content_type("application/json")
669 |> send_resp(403, Jason.encode!(%{"error" => message}))
673 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
674 with %User{} = follower <- Repo.get(User, id),
675 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
676 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
678 ActivityPub.reject(%{
679 to: [follower.ap_id],
680 actor: followed.ap_id,
681 object: follow_activity.data["id"],
685 |> put_view(AccountView)
686 |> render("relationship.json", %{user: followed, target: follower})
690 |> put_resp_content_type("application/json")
691 |> send_resp(403, Jason.encode!(%{"error" => message}))
695 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
696 with %User{} = followed <- Repo.get(User, id),
697 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
698 {:ok, _activity} <- ActivityPub.follow(follower, followed),
699 {:ok, follower, followed} <-
700 User.wait_and_refresh(
701 Config.get([:activitypub, :follow_handshake_timeout]),
706 |> put_view(AccountView)
707 |> render("relationship.json", %{user: follower, target: followed})
711 |> put_resp_content_type("application/json")
712 |> send_resp(403, Jason.encode!(%{"error" => message}))
716 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
717 with %User{} = followed <- Repo.get_by(User, nickname: uri),
718 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
719 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
721 |> put_view(AccountView)
722 |> render("account.json", %{user: followed, for: follower})
726 |> put_resp_content_type("application/json")
727 |> send_resp(403, Jason.encode!(%{"error" => message}))
731 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
732 with %User{} = followed <- Repo.get(User, id),
733 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
734 {:ok, follower, _} <- User.unfollow(follower, followed) do
736 |> put_view(AccountView)
737 |> render("relationship.json", %{user: follower, target: followed})
741 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
742 with %User{} = blocked <- Repo.get(User, id),
743 {:ok, blocker} <- User.block(blocker, blocked),
744 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
746 |> put_view(AccountView)
747 |> render("relationship.json", %{user: blocker, target: blocked})
751 |> put_resp_content_type("application/json")
752 |> send_resp(403, Jason.encode!(%{"error" => message}))
756 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
757 with %User{} = blocked <- Repo.get(User, id),
758 {:ok, blocker} <- User.unblock(blocker, blocked),
759 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
761 |> put_view(AccountView)
762 |> render("relationship.json", %{user: blocker, target: blocked})
766 |> put_resp_content_type("application/json")
767 |> send_resp(403, Jason.encode!(%{"error" => message}))
771 def blocks(%{assigns: %{user: user}} = conn, _) do
772 with blocked_accounts <- User.blocked_users(user) do
773 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
778 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
779 json(conn, info.domain_blocks || [])
782 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
783 User.block_domain(blocker, domain)
787 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
788 User.unblock_domain(blocker, domain)
792 def status_search(user, query) do
794 if Regex.match?(~r/https?:/, query) do
795 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
796 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
797 true <- ActivityPub.visible_for_user?(activity, user) do
807 where: fragment("?->>'type' = 'Create'", a.data),
808 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
811 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
816 order_by: [desc: :id]
819 Repo.all(q) ++ fetched
822 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
823 accounts = User.search(query, params["resolve"] == "true", user)
825 statuses = status_search(user, query)
827 tags_path = Web.base_url() <> "/tag/"
833 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
834 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
835 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
838 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
840 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
847 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
848 accounts = User.search(query, params["resolve"] == "true", user)
850 statuses = status_search(user, query)
856 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
857 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
860 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
862 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
869 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
870 accounts = User.search(query, params["resolve"] == "true", user)
872 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
877 def favourites(%{assigns: %{user: user}} = conn, params) do
880 |> Map.put("type", "Create")
881 |> Map.put("favorited_by", user.ap_id)
882 |> Map.put("blocking_user", user)
883 |> ActivityPub.fetch_public_activities()
887 |> add_link_headers(:favourites, activities)
888 |> put_view(StatusView)
889 |> render("index.json", %{activities: activities, for: user, as: :activity})
892 def bookmarks(%{assigns: %{user: user}} = conn, _) do
893 user = Repo.get(User, user.id)
897 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
901 |> put_view(StatusView)
902 |> render("index.json", %{activities: activities, for: user, as: :activity})
905 def get_lists(%{assigns: %{user: user}} = conn, opts) do
906 lists = Pleroma.List.for_user(user, opts)
907 res = ListView.render("lists.json", lists: lists)
911 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
912 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
913 res = ListView.render("list.json", list: list)
919 |> json(%{error: "Record not found"})
923 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
924 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
925 res = ListView.render("lists.json", lists: lists)
929 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
930 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
931 {:ok, _list} <- Pleroma.List.delete(list) do
939 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
940 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
941 res = ListView.render("list.json", list: list)
946 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
948 |> Enum.each(fn account_id ->
949 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
950 %User{} = followed <- Repo.get(User, account_id) do
951 Pleroma.List.follow(list, followed)
958 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
960 |> Enum.each(fn account_id ->
961 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
962 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
963 Pleroma.List.unfollow(list, followed)
970 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
971 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
972 {:ok, users} = Pleroma.List.get_following(list) do
974 |> put_view(AccountView)
975 |> render("accounts.json", %{users: users, as: :user})
979 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
980 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
981 {:ok, list} <- Pleroma.List.rename(list, title) do
982 res = ListView.render("list.json", list: list)
990 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
991 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
994 |> Map.put("type", "Create")
995 |> Map.put("blocking_user", user)
997 # we must filter the following list for the user to avoid leaking statuses the user
998 # does not actually have permission to see (for more info, peruse security issue #270).
1001 |> Enum.filter(fn x -> x in user.following end)
1002 |> ActivityPub.fetch_activities_bounded(following, params)
1006 |> put_view(StatusView)
1007 |> render("index.json", %{activities: activities, for: user, as: :activity})
1012 |> json(%{error: "Error."})
1016 def index(%{assigns: %{user: user}} = conn, _params) do
1019 |> get_session(:oauth_token)
1022 mastodon_emoji = mastodonized_emoji()
1024 limit = Config.get([:instance, :limit])
1027 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1032 streaming_api_base_url:
1033 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1034 access_token: token,
1036 domain: Pleroma.Web.Endpoint.host(),
1039 unfollow_modal: false,
1042 auto_play_gif: false,
1043 display_sensitive_media: false,
1044 reduce_motion: false,
1045 max_toot_chars: limit
1048 delete_others_notice: present?(user.info.is_moderator),
1049 admin: present?(user.info.is_admin)
1053 default_privacy: user.info.default_scope,
1054 default_sensitive: false
1056 media_attachments: %{
1057 accept_content_types: [
1073 user.info.settings ||
1103 push_subscription: nil,
1105 custom_emojis: mastodon_emoji,
1111 |> put_layout(false)
1112 |> put_view(MastodonView)
1113 |> render("index.html", %{initial_state: initial_state})
1116 |> redirect(to: "/web/login")
1120 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1121 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1123 with changeset <- Ecto.Changeset.change(user),
1124 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1125 {:ok, _user} <- User.update_and_set_cache(changeset) do
1130 |> put_resp_content_type("application/json")
1131 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1135 def login(conn, %{"code" => code}) do
1136 with {:ok, app} <- get_or_make_app(),
1137 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1138 {:ok, token} <- Token.exchange_token(app, auth) do
1140 |> put_session(:oauth_token, token.token)
1141 |> redirect(to: "/web/getting-started")
1145 def login(conn, _) do
1146 with {:ok, app} <- get_or_make_app() do
1151 response_type: "code",
1152 client_id: app.client_id,
1158 |> redirect(to: path)
1162 defp get_or_make_app() do
1163 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1165 with %App{} = app <- Repo.get_by(App, find_attrs) do
1169 cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
1175 def logout(conn, _) do
1178 |> redirect(to: "/")
1181 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1182 Logger.debug("Unimplemented, returning unmodified relationship")
1184 with %User{} = target <- Repo.get(User, id) do
1186 |> put_view(AccountView)
1187 |> render("relationship.json", %{user: user, target: target})
1191 def empty_array(conn, _) do
1192 Logger.debug("Unimplemented, returning an empty array")
1196 def empty_object(conn, _) do
1197 Logger.debug("Unimplemented, returning an empty object")
1201 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1202 actor = User.get_cached_by_ap_id(activity.data["actor"])
1203 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1204 mastodon_type = Activity.mastodon_notification_type(activity)
1208 type: mastodon_type,
1209 created_at: CommonAPI.Utils.to_masto_date(created_at),
1210 account: AccountView.render("account.json", %{user: actor, for: user})
1213 case mastodon_type do
1217 status: StatusView.render("status.json", %{activity: activity, for: user})
1223 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1229 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1240 def get_filters(%{assigns: %{user: user}} = conn, _) do
1241 filters = Filter.get_filters(user)
1242 res = FilterView.render("filters.json", filters: filters)
1247 %{assigns: %{user: user}} = conn,
1248 %{"phrase" => phrase, "context" => context} = params
1254 hide: Map.get(params, "irreversible", nil),
1255 whole_word: Map.get(params, "boolean", true)
1259 {:ok, response} = Filter.create(query)
1260 res = FilterView.render("filter.json", filter: response)
1264 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1265 filter = Filter.get(filter_id, user)
1266 res = FilterView.render("filter.json", filter: filter)
1271 %{assigns: %{user: user}} = conn,
1272 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1276 filter_id: filter_id,
1279 hide: Map.get(params, "irreversible", nil),
1280 whole_word: Map.get(params, "boolean", true)
1284 {:ok, response} = Filter.update(query)
1285 res = FilterView.render("filter.json", filter: response)
1289 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1292 filter_id: filter_id
1295 {:ok, _} = Filter.delete(query)
1299 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1300 true = Push.enabled()
1301 Subscription.delete_if_exists(user, token)
1302 {:ok, subscription} = Subscription.create(user, token, params)
1303 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1307 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1308 true = Push.enabled()
1309 subscription = Subscription.get(user, token)
1310 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1314 def update_push_subscription(
1315 %{assigns: %{user: user, token: token}} = conn,
1318 true = Push.enabled()
1319 {:ok, subscription} = Subscription.update(user, token, params)
1320 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1324 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1325 true = Push.enabled()
1326 {:ok, _response} = Subscription.delete(user, token)
1330 def errors(conn, _) do
1333 |> json("Something went wrong")
1336 def suggestions(%{assigns: %{user: user}} = conn, _) do
1337 suggestions = Config.get(:suggestions)
1339 if Keyword.get(suggestions, :enabled, false) do
1340 api = Keyword.get(suggestions, :third_party_engine, "")
1341 timeout = Keyword.get(suggestions, :timeout, 5000)
1342 limit = Keyword.get(suggestions, :limit, 23)
1344 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1346 user = user.nickname
1350 |> String.replace("{{host}}", host)
1351 |> String.replace("{{user}}", user)
1353 with {:ok, %{status: 200, body: body}} <-
1359 recv_timeout: timeout,
1363 {:ok, data} <- Jason.decode(body) do
1366 |> Enum.slice(0, limit)
1371 case User.get_or_fetch(x["acct"]) do
1378 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1381 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1387 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1394 def status_card(conn, %{"id" => status_id}) do
1395 with %Activity{} = activity <- Repo.get(Activity, status_id),
1396 true <- ActivityPub.is_public?(activity) do
1400 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1410 def try_render(conn, target, params)
1411 when is_binary(target) do
1412 res = render(conn, target, params)
1417 |> json(%{error: "Can't display this activity"})
1423 def try_render(conn, _, _) do
1426 |> json(%{error: "Can't display this activity"})
1429 defp present?(nil), do: false
1430 defp present?(false), do: false
1431 defp present?(_), do: true