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
1115 media_attachments: %{
1116 accept_content_types: [
1132 user.info.settings ||
1162 push_subscription: nil,
1164 custom_emojis: mastodon_emoji,
1170 |> put_layout(false)
1171 |> put_view(MastodonView)
1172 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1175 |> redirect(to: "/web/login")
1179 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1180 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1182 with changeset <- Ecto.Changeset.change(user),
1183 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1184 {:ok, _user} <- User.update_and_set_cache(changeset) do
1189 |> put_resp_content_type("application/json")
1190 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1194 @supported_flavours ["glitch", "vanilla"]
1196 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1197 when flavour in @supported_flavours do
1198 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1200 with changeset <- Ecto.Changeset.change(user),
1201 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1202 {:ok, user} <- User.update_and_set_cache(changeset),
1203 flavour <- user.info.flavour do
1208 |> put_resp_content_type("application/json")
1209 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1213 def set_flavour(conn, _params) do
1216 |> json(%{error: "Unsupported flavour"})
1219 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1220 json(conn, get_user_flavour(user))
1223 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1227 defp get_user_flavour(_) do
1231 def login(conn, %{"code" => code}) do
1232 with {:ok, app} <- get_or_make_app(),
1233 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1234 {:ok, token} <- Token.exchange_token(app, auth) do
1236 |> put_session(:oauth_token, token.token)
1237 |> redirect(to: "/web/getting-started")
1241 def login(conn, _) do
1242 with {:ok, app} <- get_or_make_app() do
1247 response_type: "code",
1248 client_id: app.client_id,
1250 scope: Enum.join(app.scopes, " ")
1254 |> redirect(to: path)
1258 defp get_or_make_app do
1259 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1260 scopes = ["read", "write", "follow", "push"]
1262 with %App{} = app <- Repo.get_by(App, find_attrs) do
1264 if app.scopes == scopes do
1268 |> Ecto.Changeset.change(%{scopes: scopes})
1276 App.register_changeset(
1278 Map.put(find_attrs, :scopes, scopes)
1285 def logout(conn, _) do
1288 |> redirect(to: "/")
1291 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1292 Logger.debug("Unimplemented, returning unmodified relationship")
1294 with %User{} = target <- Repo.get(User, id) do
1296 |> put_view(AccountView)
1297 |> render("relationship.json", %{user: user, target: target})
1301 def empty_array(conn, _) do
1302 Logger.debug("Unimplemented, returning an empty array")
1306 def empty_object(conn, _) do
1307 Logger.debug("Unimplemented, returning an empty object")
1311 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1312 actor = User.get_cached_by_ap_id(activity.data["actor"])
1313 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1314 mastodon_type = Activity.mastodon_notification_type(activity)
1318 type: mastodon_type,
1319 created_at: CommonAPI.Utils.to_masto_date(created_at),
1320 account: AccountView.render("account.json", %{user: actor, for: user})
1323 case mastodon_type do
1327 status: StatusView.render("status.json", %{activity: activity, for: user})
1333 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1339 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1350 def get_filters(%{assigns: %{user: user}} = conn, _) do
1351 filters = Filter.get_filters(user)
1352 res = FilterView.render("filters.json", filters: filters)
1357 %{assigns: %{user: user}} = conn,
1358 %{"phrase" => phrase, "context" => context} = params
1364 hide: Map.get(params, "irreversible", nil),
1365 whole_word: Map.get(params, "boolean", true)
1369 {:ok, response} = Filter.create(query)
1370 res = FilterView.render("filter.json", filter: response)
1374 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1375 filter = Filter.get(filter_id, user)
1376 res = FilterView.render("filter.json", filter: filter)
1381 %{assigns: %{user: user}} = conn,
1382 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1386 filter_id: filter_id,
1389 hide: Map.get(params, "irreversible", nil),
1390 whole_word: Map.get(params, "boolean", true)
1394 {:ok, response} = Filter.update(query)
1395 res = FilterView.render("filter.json", filter: response)
1399 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1402 filter_id: filter_id
1405 {:ok, _} = Filter.delete(query)
1411 def errors(conn, _) do
1414 |> json("Something went wrong")
1417 def suggestions(%{assigns: %{user: user}} = conn, _) do
1418 suggestions = Config.get(:suggestions)
1420 if Keyword.get(suggestions, :enabled, false) do
1421 api = Keyword.get(suggestions, :third_party_engine, "")
1422 timeout = Keyword.get(suggestions, :timeout, 5000)
1423 limit = Keyword.get(suggestions, :limit, 23)
1425 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1427 user = user.nickname
1431 |> String.replace("{{host}}", host)
1432 |> String.replace("{{user}}", user)
1434 with {:ok, %{status: 200, body: body}} <-
1439 recv_timeout: timeout,
1443 {:ok, data} <- Jason.decode(body) do
1446 |> Enum.slice(0, limit)
1451 case User.get_or_fetch(x["acct"]) do
1458 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1461 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1467 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1474 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1475 with %Activity{} = activity <- Repo.get(Activity, status_id),
1476 true <- Visibility.visible_for_user?(activity, user) do
1480 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1490 def reports(%{assigns: %{user: user}} = conn, params) do
1491 case CommonAPI.report(user, params) do
1494 |> put_view(ReportView)
1495 |> try_render("report.json", %{activity: activity})
1499 |> put_status(:bad_request)
1500 |> json(%{error: err})
1504 def try_render(conn, target, params)
1505 when is_binary(target) do
1506 res = render(conn, target, params)
1511 |> json(%{error: "Can't display this activity"})
1517 def try_render(conn, _, _) do
1520 |> json(%{error: "Can't display this activity"})
1523 defp present?(nil), do: false
1524 defp present?(false), do: false
1525 defp present?(_), do: true