1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Notification
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.FilterView
22 alias Pleroma.Web.MastodonAPI.ListView
23 alias Pleroma.Web.MastodonAPI.MastodonAPI
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.ReportView
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.MediaProxy
28 alias Pleroma.Web.OAuth.App
29 alias Pleroma.Web.OAuth.Authorization
30 alias Pleroma.Web.OAuth.Token
32 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
37 @httpoison Application.get_env(:pleroma, :httpoison)
38 @local_mastodon_name "Mastodon-Local"
40 action_fallback(:errors)
42 def create_app(conn, params) do
43 scopes = oauth_scopes(params, ["read"])
47 |> Map.drop(["scope", "scopes"])
48 |> Map.put("scopes", scopes)
50 with cs <- App.register_changeset(%App{}, app_attrs),
51 false <- cs.changes[:client_name] == @local_mastodon_name,
52 {:ok, app} <- Repo.insert(cs) do
54 id: app.id |> to_string,
55 name: app.client_name,
56 client_id: app.client_id,
57 client_secret: app.client_secret,
58 redirect_uri: app.redirect_uris,
71 value_function \\ fn x -> {:ok, x} end
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
101 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
102 |> add_if_present(params, "header", :banner, fn value ->
103 with %Plug.Upload{} <- value,
104 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
111 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113 with changeset <- User.update_changeset(user, user_params),
114 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
115 {:ok, user} <- User.update_and_set_cache(changeset) do
116 if original_user != user do
117 CommonAPI.update(user)
120 json(conn, AccountView.render("account.json", %{user: user, for: user}))
125 |> json(%{error: "Invalid request"})
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 account = AccountView.render("account.json", %{user: user, for: user})
134 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
135 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
136 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
137 account = AccountView.render("account.json", %{user: user, for: for_user})
143 |> json(%{error: "Can't find user"})
147 @mastodon_api_level "2.5.0"
149 def masto_instance(conn, _params) do
150 instance = Config.get(:instance)
154 title: Keyword.get(instance, :name),
155 description: Keyword.get(instance, :description),
156 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
157 email: Keyword.get(instance, :email),
159 streaming_api: Pleroma.Web.Endpoint.websocket_url()
161 stats: Stats.get_stats(),
162 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
163 max_toot_chars: Keyword.get(instance, :limit)
169 def peers(conn, _params) do
170 json(conn, Stats.get_peers())
173 defp mastodonized_emoji do
174 Pleroma.Emoji.get_all()
175 |> Enum.map(fn {shortcode, relative_url} ->
176 url = to_string(URI.merge(Web.base_url(), relative_url))
179 "shortcode" => shortcode,
181 "visible_in_picker" => true,
187 def custom_emojis(conn, _params) do
188 mastodon_emoji = mastodonized_emoji()
189 json(conn, mastodon_emoji)
192 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
195 |> Map.drop(["since_id", "max_id"])
198 last = List.last(activities)
199 first = List.first(activities)
205 {next_url, prev_url} =
209 Pleroma.Web.Endpoint,
212 Map.merge(params, %{max_id: min})
215 Pleroma.Web.Endpoint,
218 Map.merge(params, %{since_id: max})
224 Pleroma.Web.Endpoint,
226 Map.merge(params, %{max_id: min})
229 Pleroma.Web.Endpoint,
231 Map.merge(params, %{since_id: max})
237 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
243 def home_timeline(%{assigns: %{user: user}} = conn, params) do
246 |> Map.put("type", ["Create", "Announce"])
247 |> Map.put("blocking_user", user)
248 |> Map.put("muting_user", user)
249 |> Map.put("user", user)
252 [user.ap_id | user.following]
253 |> ActivityPub.fetch_activities(params)
254 |> ActivityPub.contain_timeline(user)
258 |> add_link_headers(:home_timeline, activities)
259 |> put_view(StatusView)
260 |> render("index.json", %{activities: activities, for: user, as: :activity})
263 def public_timeline(%{assigns: %{user: user}} = conn, params) do
264 local_only = params["local"] in [true, "True", "true", "1"]
268 |> Map.put("type", ["Create", "Announce"])
269 |> Map.put("local_only", local_only)
270 |> Map.put("blocking_user", user)
271 |> Map.put("muting_user", user)
272 |> ActivityPub.fetch_public_activities()
276 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
277 |> put_view(StatusView)
278 |> render("index.json", %{activities: activities, for: user, as: :activity})
281 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
282 with %User{} = user <- Repo.get(User, params["id"]) do
283 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
286 |> add_link_headers(:user_statuses, activities, params["id"])
287 |> put_view(StatusView)
288 |> render("index.json", %{
289 activities: activities,
296 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
299 |> Map.put("type", "Create")
300 |> Map.put("blocking_user", user)
301 |> Map.put("user", user)
302 |> Map.put(:visibility, "direct")
306 |> ActivityPub.fetch_activities_query(params)
310 |> add_link_headers(:dm_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
316 with %Activity{} = activity <- Repo.get(Activity, id),
317 true <- Visibility.visible_for_user?(activity, user) do
319 |> put_view(StatusView)
320 |> try_render("status.json", %{activity: activity, for: user})
324 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
325 with %Activity{} = activity <- Repo.get(Activity, id),
327 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
328 "blocking_user" => user,
332 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
334 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
335 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
341 activities: grouped_activities[true] || [],
345 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
350 activities: grouped_activities[false] || [],
354 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
361 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
362 when length(media_ids) > 0 do
365 |> Map.put("status", ".")
367 post_status(conn, params)
370 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
373 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
376 case get_req_header(conn, "idempotency-key") do
378 _ -> Ecto.UUID.generate()
382 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
385 |> put_view(StatusView)
386 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
389 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
390 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
396 |> json(%{error: "Can't delete this post"})
400 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
401 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
403 |> put_view(StatusView)
404 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
408 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
409 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
410 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
412 |> put_view(StatusView)
413 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
417 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
418 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
419 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
421 |> put_view(StatusView)
422 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
426 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
427 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
428 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
430 |> put_view(StatusView)
431 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
435 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
436 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
438 |> put_view(StatusView)
439 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
443 |> put_resp_content_type("application/json")
444 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
448 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
449 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
451 |> put_view(StatusView)
452 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
456 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
457 with %Activity{} = activity <- Repo.get(Activity, id),
458 %User{} = user <- User.get_by_nickname(user.nickname),
459 true <- Visibility.visible_for_user?(activity, user),
460 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
462 |> put_view(StatusView)
463 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
467 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
468 with %Activity{} = activity <- Repo.get(Activity, id),
469 %User{} = user <- User.get_by_nickname(user.nickname),
470 true <- Visibility.visible_for_user?(activity, user),
471 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 activity = Activity.get_by_id(id)
481 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
483 |> put_view(StatusView)
484 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
488 |> put_resp_content_type("application/json")
489 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
493 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
494 activity = Activity.get_by_id(id)
496 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
498 |> put_view(StatusView)
499 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
503 def notifications(%{assigns: %{user: user}} = conn, params) do
504 notifications = Notification.for_user(user, params)
508 |> Enum.map(fn x -> render_notification(user, x) end)
512 |> add_link_headers(:notifications, notifications)
516 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
517 with {:ok, notification} <- Notification.get(user, id) do
518 json(conn, render_notification(user, notification))
522 |> put_resp_content_type("application/json")
523 |> send_resp(403, Jason.encode!(%{"error" => reason}))
527 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
528 Notification.clear(user)
532 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
533 with {:ok, _notif} <- Notification.dismiss(user, id) do
538 |> put_resp_content_type("application/json")
539 |> send_resp(403, Jason.encode!(%{"error" => reason}))
543 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
545 q = from(u in User, where: u.id in ^id)
546 targets = Repo.all(q)
549 |> put_view(AccountView)
550 |> render("relationships.json", %{user: user, targets: targets})
553 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
554 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
556 def update_media(%{assigns: %{user: user}} = conn, data) do
557 with %Object{} = object <- Repo.get(Object, data["id"]),
558 true <- Object.authorize_mutation(object, user),
559 true <- is_binary(data["description"]),
560 description <- data["description"] do
561 new_data = %{object.data | "name" => description}
565 |> Object.change(%{data: new_data})
568 attachment_data = Map.put(new_data, "id", object.id)
571 |> put_view(StatusView)
572 |> render("attachment.json", %{attachment: attachment_data})
576 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
577 with {:ok, object} <-
580 actor: User.ap_id(user),
581 description: Map.get(data, "description")
583 attachment_data = Map.put(object.data, "id", object.id)
586 |> put_view(StatusView)
587 |> render("attachment.json", %{attachment: attachment_data})
591 def favourited_by(conn, %{"id" => id}) do
592 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
593 q = from(u in User, where: u.ap_id in ^likes)
597 |> put_view(AccountView)
598 |> render(AccountView, "accounts.json", %{users: users, as: :user})
604 def reblogged_by(conn, %{"id" => id}) do
605 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
606 q = from(u in User, where: u.ap_id in ^announces)
610 |> put_view(AccountView)
611 |> render("accounts.json", %{users: users, as: :user})
617 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
618 local_only = params["local"] in [true, "True", "true", "1"]
621 [params["tag"], params["any"]]
625 |> Enum.map(&String.downcase(&1))
630 |> Enum.map(&String.downcase(&1))
635 |> Enum.map(&String.downcase(&1))
639 |> Map.put("type", "Create")
640 |> Map.put("local_only", local_only)
641 |> Map.put("blocking_user", user)
642 |> Map.put("muting_user", user)
643 |> Map.put("tag", tags)
644 |> Map.put("tag_all", tag_all)
645 |> Map.put("tag_reject", tag_reject)
646 |> ActivityPub.fetch_public_activities()
650 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
651 |> put_view(StatusView)
652 |> render("index.json", %{activities: activities, for: user, as: :activity})
655 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
656 with %User{} = user <- Repo.get(User, id),
657 followers <- MastodonAPI.get_followers(user, params) do
660 for_user && user.id == for_user.id -> followers
661 user.info.hide_followers -> []
666 |> add_link_headers(:followers, followers, user)
667 |> put_view(AccountView)
668 |> render("accounts.json", %{users: followers, as: :user})
672 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
673 with %User{} = user <- Repo.get(User, id),
674 followers <- MastodonAPI.get_friends(user, params) do
677 for_user && user.id == for_user.id -> followers
678 user.info.hide_follows -> []
683 |> add_link_headers(:following, followers, user)
684 |> put_view(AccountView)
685 |> render("accounts.json", %{users: followers, as: :user})
689 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
690 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
692 |> put_view(AccountView)
693 |> render("accounts.json", %{users: follow_requests, as: :user})
697 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
698 with %User{} = follower <- Repo.get(User, id),
699 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
701 |> put_view(AccountView)
702 |> render("relationship.json", %{user: followed, target: follower})
706 |> put_resp_content_type("application/json")
707 |> send_resp(403, Jason.encode!(%{"error" => message}))
711 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
712 with %User{} = follower <- Repo.get(User, id),
713 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
715 |> put_view(AccountView)
716 |> render("relationship.json", %{user: followed, target: follower})
720 |> put_resp_content_type("application/json")
721 |> send_resp(403, Jason.encode!(%{"error" => message}))
725 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
726 with %User{} = followed <- Repo.get(User, id),
727 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
729 |> put_view(AccountView)
730 |> render("relationship.json", %{user: follower, target: followed})
734 |> put_resp_content_type("application/json")
735 |> send_resp(403, Jason.encode!(%{"error" => message}))
739 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
740 with %User{} = followed <- Repo.get_by(User, nickname: uri),
741 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
743 |> put_view(AccountView)
744 |> render("account.json", %{user: followed, for: follower})
748 |> put_resp_content_type("application/json")
749 |> send_resp(403, Jason.encode!(%{"error" => message}))
753 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
754 with %User{} = followed <- Repo.get(User, id),
755 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
757 |> put_view(AccountView)
758 |> render("relationship.json", %{user: follower, target: followed})
762 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
763 with %User{} = muted <- Repo.get(User, id),
764 {:ok, muter} <- User.mute(muter, muted) do
766 |> put_view(AccountView)
767 |> render("relationship.json", %{user: muter, target: muted})
771 |> put_resp_content_type("application/json")
772 |> send_resp(403, Jason.encode!(%{"error" => message}))
776 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
777 with %User{} = muted <- Repo.get(User, id),
778 {:ok, muter} <- User.unmute(muter, muted) do
780 |> put_view(AccountView)
781 |> render("relationship.json", %{user: muter, target: muted})
785 |> put_resp_content_type("application/json")
786 |> send_resp(403, Jason.encode!(%{"error" => message}))
790 def mutes(%{assigns: %{user: user}} = conn, _) do
791 with muted_accounts <- User.muted_users(user) do
792 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
797 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
798 with %User{} = blocked <- Repo.get(User, id),
799 {:ok, blocker} <- User.block(blocker, blocked),
800 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
802 |> put_view(AccountView)
803 |> render("relationship.json", %{user: blocker, target: blocked})
807 |> put_resp_content_type("application/json")
808 |> send_resp(403, Jason.encode!(%{"error" => message}))
812 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
813 with %User{} = blocked <- Repo.get(User, id),
814 {:ok, blocker} <- User.unblock(blocker, blocked),
815 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
817 |> put_view(AccountView)
818 |> render("relationship.json", %{user: blocker, target: blocked})
822 |> put_resp_content_type("application/json")
823 |> send_resp(403, Jason.encode!(%{"error" => message}))
827 def blocks(%{assigns: %{user: user}} = conn, _) do
828 with blocked_accounts <- User.blocked_users(user) do
829 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
834 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
835 json(conn, info.domain_blocks || [])
838 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
839 User.block_domain(blocker, domain)
843 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
844 User.unblock_domain(blocker, domain)
848 def status_search(user, query) do
850 if Regex.match?(~r/https?:/, query) do
851 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
852 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
853 true <- Visibility.visible_for_user?(activity, user) do
863 where: fragment("?->>'type' = 'Create'", a.data),
864 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
867 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
872 order_by: [desc: :id]
875 Repo.all(q) ++ fetched
878 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
879 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
881 statuses = status_search(user, query)
883 tags_path = Web.base_url() <> "/tag/"
889 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
890 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
891 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
894 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
896 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
903 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
904 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
906 statuses = status_search(user, query)
912 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
913 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
916 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
918 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
925 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
926 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
928 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
933 def favourites(%{assigns: %{user: user}} = conn, params) do
936 |> Map.put("type", "Create")
937 |> Map.put("favorited_by", user.ap_id)
938 |> Map.put("blocking_user", user)
939 |> ActivityPub.fetch_public_activities()
943 |> add_link_headers(:favourites, activities)
944 |> put_view(StatusView)
945 |> render("index.json", %{activities: activities, for: user, as: :activity})
948 def bookmarks(%{assigns: %{user: user}} = conn, _) do
949 user = Repo.get(User, user.id)
953 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
957 |> put_view(StatusView)
958 |> render("index.json", %{activities: activities, for: user, as: :activity})
961 def get_lists(%{assigns: %{user: user}} = conn, opts) do
962 lists = Pleroma.List.for_user(user, opts)
963 res = ListView.render("lists.json", lists: lists)
967 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
968 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
969 res = ListView.render("list.json", list: list)
975 |> json(%{error: "Record not found"})
979 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
980 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
981 res = ListView.render("lists.json", lists: lists)
985 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
987 {:ok, _list} <- Pleroma.List.delete(list) do
995 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
996 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
997 res = ListView.render("list.json", list: list)
1002 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1004 |> Enum.each(fn account_id ->
1005 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1006 %User{} = followed <- Repo.get(User, account_id) do
1007 Pleroma.List.follow(list, followed)
1014 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1016 |> Enum.each(fn account_id ->
1017 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1018 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1019 Pleroma.List.unfollow(list, followed)
1026 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1027 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1028 {:ok, users} = Pleroma.List.get_following(list) do
1030 |> put_view(AccountView)
1031 |> render("accounts.json", %{users: users, as: :user})
1035 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1036 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1037 {:ok, list} <- Pleroma.List.rename(list, title) do
1038 res = ListView.render("list.json", list: list)
1046 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1047 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1050 |> Map.put("type", "Create")
1051 |> Map.put("blocking_user", user)
1052 |> Map.put("muting_user", user)
1054 # we must filter the following list for the user to avoid leaking statuses the user
1055 # does not actually have permission to see (for more info, peruse security issue #270).
1058 |> Enum.filter(fn x -> x in user.following end)
1059 |> ActivityPub.fetch_activities_bounded(following, params)
1063 |> put_view(StatusView)
1064 |> render("index.json", %{activities: activities, for: user, as: :activity})
1069 |> json(%{error: "Error."})
1073 def index(%{assigns: %{user: user}} = conn, _params) do
1076 |> get_session(:oauth_token)
1079 mastodon_emoji = mastodonized_emoji()
1081 limit = Config.get([:instance, :limit])
1084 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1086 flavour = get_user_flavour(user)
1091 streaming_api_base_url:
1092 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1093 access_token: token,
1095 domain: Pleroma.Web.Endpoint.host(),
1098 unfollow_modal: false,
1101 auto_play_gif: false,
1102 display_sensitive_media: false,
1103 reduce_motion: false,
1104 max_toot_chars: limit
1107 delete_others_notice: present?(user.info.is_moderator),
1108 admin: present?(user.info.is_admin)
1112 default_privacy: user.info.default_scope,
1113 default_sensitive: false,
1114 allow_content_types: Config.get([:instance, :allowed_post_formats])
1116 media_attachments: %{
1117 accept_content_types: [
1133 user.info.settings ||
1163 push_subscription: nil,
1165 custom_emojis: mastodon_emoji,
1171 |> put_layout(false)
1172 |> put_view(MastodonView)
1173 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1176 |> redirect(to: "/web/login")
1180 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1181 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1183 with changeset <- Ecto.Changeset.change(user),
1184 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1185 {:ok, _user} <- User.update_and_set_cache(changeset) do
1190 |> put_resp_content_type("application/json")
1191 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1195 @supported_flavours ["glitch", "vanilla"]
1197 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1198 when flavour in @supported_flavours do
1199 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1201 with changeset <- Ecto.Changeset.change(user),
1202 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1203 {:ok, user} <- User.update_and_set_cache(changeset),
1204 flavour <- user.info.flavour do
1209 |> put_resp_content_type("application/json")
1210 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1214 def set_flavour(conn, _params) do
1217 |> json(%{error: "Unsupported flavour"})
1220 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1221 json(conn, get_user_flavour(user))
1224 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1228 defp get_user_flavour(_) do
1232 def login(conn, %{"code" => code}) do
1233 with {:ok, app} <- get_or_make_app(),
1234 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1235 {:ok, token} <- Token.exchange_token(app, auth) do
1237 |> put_session(:oauth_token, token.token)
1238 |> redirect(to: "/web/getting-started")
1242 def login(conn, _) do
1243 with {:ok, app} <- get_or_make_app() do
1248 response_type: "code",
1249 client_id: app.client_id,
1251 scope: Enum.join(app.scopes, " ")
1255 |> redirect(to: path)
1259 defp get_or_make_app do
1260 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1261 scopes = ["read", "write", "follow", "push"]
1263 with %App{} = app <- Repo.get_by(App, find_attrs) do
1265 if app.scopes == scopes do
1269 |> Ecto.Changeset.change(%{scopes: scopes})
1277 App.register_changeset(
1279 Map.put(find_attrs, :scopes, scopes)
1286 def logout(conn, _) do
1289 |> redirect(to: "/")
1292 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1293 Logger.debug("Unimplemented, returning unmodified relationship")
1295 with %User{} = target <- Repo.get(User, id) do
1297 |> put_view(AccountView)
1298 |> render("relationship.json", %{user: user, target: target})
1302 def empty_array(conn, _) do
1303 Logger.debug("Unimplemented, returning an empty array")
1307 def empty_object(conn, _) do
1308 Logger.debug("Unimplemented, returning an empty object")
1312 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1313 actor = User.get_cached_by_ap_id(activity.data["actor"])
1314 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1315 mastodon_type = Activity.mastodon_notification_type(activity)
1319 type: mastodon_type,
1320 created_at: CommonAPI.Utils.to_masto_date(created_at),
1321 account: AccountView.render("account.json", %{user: actor, for: user})
1324 case mastodon_type do
1328 status: StatusView.render("status.json", %{activity: activity, for: user})
1334 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1340 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1351 def get_filters(%{assigns: %{user: user}} = conn, _) do
1352 filters = Filter.get_filters(user)
1353 res = FilterView.render("filters.json", filters: filters)
1358 %{assigns: %{user: user}} = conn,
1359 %{"phrase" => phrase, "context" => context} = params
1365 hide: Map.get(params, "irreversible", nil),
1366 whole_word: Map.get(params, "boolean", true)
1370 {:ok, response} = Filter.create(query)
1371 res = FilterView.render("filter.json", filter: response)
1375 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1376 filter = Filter.get(filter_id, user)
1377 res = FilterView.render("filter.json", filter: filter)
1382 %{assigns: %{user: user}} = conn,
1383 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1387 filter_id: filter_id,
1390 hide: Map.get(params, "irreversible", nil),
1391 whole_word: Map.get(params, "boolean", true)
1395 {:ok, response} = Filter.update(query)
1396 res = FilterView.render("filter.json", filter: response)
1400 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1403 filter_id: filter_id
1406 {:ok, _} = Filter.delete(query)
1412 def errors(conn, _) do
1415 |> json("Something went wrong")
1418 def suggestions(%{assigns: %{user: user}} = conn, _) do
1419 suggestions = Config.get(:suggestions)
1421 if Keyword.get(suggestions, :enabled, false) do
1422 api = Keyword.get(suggestions, :third_party_engine, "")
1423 timeout = Keyword.get(suggestions, :timeout, 5000)
1424 limit = Keyword.get(suggestions, :limit, 23)
1426 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1428 user = user.nickname
1432 |> String.replace("{{host}}", host)
1433 |> String.replace("{{user}}", user)
1435 with {:ok, %{status: 200, body: body}} <-
1440 recv_timeout: timeout,
1444 {:ok, data} <- Jason.decode(body) do
1447 |> Enum.slice(0, limit)
1452 case User.get_or_fetch(x["acct"]) do
1453 {:ok, %User{} = %{id: id}} -> id
1459 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1462 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1468 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1475 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1476 with %Activity{} = activity <- Repo.get(Activity, status_id),
1477 true <- Visibility.visible_for_user?(activity, user) do
1481 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1491 def reports(%{assigns: %{user: user}} = conn, params) do
1492 case CommonAPI.report(user, params) do
1495 |> put_view(ReportView)
1496 |> try_render("report.json", %{activity: activity})
1500 |> put_status(:bad_request)
1501 |> json(%{error: err})
1505 def try_render(conn, target, params)
1506 when is_binary(target) do
1507 res = render(conn, target, params)
1512 |> json(%{error: "Can't display this activity"})
1518 def try_render(conn, _, _) do
1521 |> json(%{error: "Can't display this activity"})
1524 defp present?(nil), do: false
1525 defp present?(false), do: false
1526 defp present?(_), do: true