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.MastodonAPI.ReportView
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Utils
30 alias Pleroma.Web.ActivityPub.Visibility
31 alias Pleroma.Web.OAuth.App
32 alias Pleroma.Web.OAuth.Authorization
33 alias Pleroma.Web.OAuth.Token
35 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
40 @httpoison Application.get_env(:pleroma, :httpoison)
41 @local_mastodon_name "Mastodon-Local"
43 action_fallback(:errors)
45 def create_app(conn, params) do
46 scopes = oauth_scopes(params, ["read"])
50 |> Map.drop(["scope", "scopes"])
51 |> Map.put("scopes", scopes)
53 with cs <- App.register_changeset(%App{}, app_attrs),
54 false <- cs.changes[:client_name] == @local_mastodon_name,
55 {:ok, app} <- Repo.insert(cs) do
57 id: app.id |> to_string,
58 name: app.client_name,
59 client_id: app.client_id,
60 client_secret: app.client_secret,
61 redirect_uri: app.redirect_uris,
74 value_function \\ fn x -> {:ok, x} end
76 if Map.has_key?(params, params_field) do
77 case value_function.(params[params_field]) do
78 {:ok, new_value} -> Map.put(map, map_field, new_value)
86 def update_credentials(%{assigns: %{user: user}} = conn, params) do
91 |> add_if_present(params, "display_name", :name)
92 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
93 |> add_if_present(params, "avatar", :avatar, fn value ->
94 with %Plug.Upload{} <- value,
95 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
104 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
105 |> add_if_present(params, "header", :banner, fn value ->
106 with %Plug.Upload{} <- value,
107 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
114 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
116 with changeset <- User.update_changeset(user, user_params),
117 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
118 {:ok, user} <- User.update_and_set_cache(changeset) do
119 if original_user != user do
120 CommonAPI.update(user)
123 json(conn, AccountView.render("account.json", %{user: user, for: user}))
128 |> json(%{error: "Invalid request"})
132 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
133 account = AccountView.render("account.json", %{user: user, for: user})
137 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
138 with %User{} = user <- Repo.get(User, id),
139 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
140 account = AccountView.render("account.json", %{user: user, for: for_user})
146 |> json(%{error: "Can't find user"})
150 @mastodon_api_level "2.5.0"
152 def masto_instance(conn, _params) do
153 instance = Config.get(:instance)
157 title: Keyword.get(instance, :name),
158 description: Keyword.get(instance, :description),
159 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
160 email: Keyword.get(instance, :email),
162 streaming_api: Pleroma.Web.Endpoint.websocket_url()
164 stats: Stats.get_stats(),
165 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
166 max_toot_chars: Keyword.get(instance, :limit)
172 def peers(conn, _params) do
173 json(conn, Stats.get_peers())
176 defp mastodonized_emoji do
177 Pleroma.Emoji.get_all()
178 |> Enum.map(fn {shortcode, relative_url} ->
179 url = to_string(URI.merge(Web.base_url(), relative_url))
182 "shortcode" => shortcode,
184 "visible_in_picker" => true,
190 def custom_emojis(conn, _params) do
191 mastodon_emoji = mastodonized_emoji()
192 json(conn, mastodon_emoji)
195 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
196 last = List.last(activities)
197 first = List.first(activities)
203 {next_url, prev_url} =
207 Pleroma.Web.Endpoint,
210 Map.merge(params, %{max_id: min})
213 Pleroma.Web.Endpoint,
216 Map.merge(params, %{since_id: max})
222 Pleroma.Web.Endpoint,
224 Map.merge(params, %{max_id: min})
227 Pleroma.Web.Endpoint,
229 Map.merge(params, %{since_id: max})
235 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
241 def home_timeline(%{assigns: %{user: user}} = conn, params) do
244 |> Map.put("type", ["Create", "Announce"])
245 |> Map.put("blocking_user", user)
246 |> Map.put("muting_user", user)
247 |> Map.put("user", user)
250 [user.ap_id | user.following]
251 |> ActivityPub.fetch_activities(params)
252 |> ActivityPub.contain_timeline(user)
256 |> add_link_headers(:home_timeline, activities)
257 |> put_view(StatusView)
258 |> render("index.json", %{activities: activities, for: user, as: :activity})
261 def public_timeline(%{assigns: %{user: user}} = conn, params) do
262 local_only = params["local"] in [true, "True", "true", "1"]
266 |> Map.put("type", ["Create", "Announce"])
267 |> Map.put("local_only", local_only)
268 |> Map.put("blocking_user", user)
269 |> Map.put("muting_user", user)
270 |> ActivityPub.fetch_public_activities()
274 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
275 |> put_view(StatusView)
276 |> render("index.json", %{activities: activities, for: user, as: :activity})
279 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
280 with %User{} = user <- Repo.get(User, params["id"]) do
281 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
284 |> add_link_headers(:user_statuses, activities, params["id"])
285 |> put_view(StatusView)
286 |> render("index.json", %{
287 activities: activities,
294 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
297 |> Map.put("type", "Create")
298 |> Map.put("blocking_user", user)
299 |> Map.put("user", user)
300 |> Map.put(:visibility, "direct")
303 ActivityPub.fetch_activities_query([user.ap_id], params)
307 |> add_link_headers(:dm_timeline, activities)
308 |> put_view(StatusView)
309 |> render("index.json", %{activities: activities, for: user, as: :activity})
312 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
313 with %Activity{} = activity <- Repo.get(Activity, id),
314 true <- Visibility.visible_for_user?(activity, user) do
316 |> put_view(StatusView)
317 |> try_render("status.json", %{activity: activity, for: user})
321 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
322 with %Activity{} = activity <- Repo.get(Activity, id),
324 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
325 "blocking_user" => user,
329 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
331 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
332 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
338 activities: grouped_activities[true] || [],
342 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
347 activities: grouped_activities[false] || [],
351 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
358 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
359 when length(media_ids) > 0 do
362 |> Map.put("status", ".")
364 post_status(conn, params)
367 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
370 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
373 case get_req_header(conn, "idempotency-key") do
375 _ -> Ecto.UUID.generate()
379 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
382 |> put_view(StatusView)
383 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
386 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
387 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
393 |> json(%{error: "Can't delete this post"})
397 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
398 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
400 |> put_view(StatusView)
401 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
405 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
406 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
407 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
409 |> put_view(StatusView)
410 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
414 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
415 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
416 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
418 |> put_view(StatusView)
419 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
423 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
424 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
425 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
427 |> put_view(StatusView)
428 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
432 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
433 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
435 |> put_view(StatusView)
436 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
440 |> put_resp_content_type("application/json")
441 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
445 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
446 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
453 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with %Activity{} = activity <- Repo.get(Activity, id),
455 %User{} = user <- User.get_by_nickname(user.nickname),
456 true <- Visibility.visible_for_user?(activity, user),
457 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
459 |> put_view(StatusView)
460 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
464 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
465 with %Activity{} = activity <- Repo.get(Activity, id),
466 %User{} = user <- User.get_by_nickname(user.nickname),
467 true <- Visibility.visible_for_user?(activity, user),
468 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
470 |> put_view(StatusView)
471 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
475 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
476 activity = Activity.get_by_id(id)
478 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
480 |> put_view(StatusView)
481 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
485 |> put_resp_content_type("application/json")
486 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
490 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
491 activity = Activity.get_by_id(id)
493 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
495 |> put_view(StatusView)
496 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
500 def notifications(%{assigns: %{user: user}} = conn, params) do
501 notifications = Notification.for_user(user, params)
505 |> Enum.map(fn x -> render_notification(user, x) end)
509 |> add_link_headers(:notifications, notifications)
513 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
514 with {:ok, notification} <- Notification.get(user, id) do
515 json(conn, render_notification(user, notification))
519 |> put_resp_content_type("application/json")
520 |> send_resp(403, Jason.encode!(%{"error" => reason}))
524 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
525 Notification.clear(user)
529 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
530 with {:ok, _notif} <- Notification.dismiss(user, id) do
535 |> put_resp_content_type("application/json")
536 |> send_resp(403, Jason.encode!(%{"error" => reason}))
540 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
542 q = from(u in User, where: u.id in ^id)
543 targets = Repo.all(q)
546 |> put_view(AccountView)
547 |> render("relationships.json", %{user: user, targets: targets})
550 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
551 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
553 def update_media(%{assigns: %{user: user}} = conn, data) do
554 with %Object{} = object <- Repo.get(Object, data["id"]),
555 true <- Object.authorize_mutation(object, user),
556 true <- is_binary(data["description"]),
557 description <- data["description"] do
558 new_data = %{object.data | "name" => description}
562 |> Object.change(%{data: new_data})
565 attachment_data = Map.put(new_data, "id", object.id)
568 |> put_view(StatusView)
569 |> render("attachment.json", %{attachment: attachment_data})
573 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
574 with {:ok, object} <-
577 actor: User.ap_id(user),
578 description: Map.get(data, "description")
580 attachment_data = Map.put(object.data, "id", object.id)
583 |> put_view(StatusView)
584 |> render("attachment.json", %{attachment: attachment_data})
588 def favourited_by(conn, %{"id" => id}) do
589 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
590 q = from(u in User, where: u.ap_id in ^likes)
594 |> put_view(AccountView)
595 |> render(AccountView, "accounts.json", %{users: users, as: :user})
601 def reblogged_by(conn, %{"id" => id}) do
602 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
603 q = from(u in User, where: u.ap_id in ^announces)
607 |> put_view(AccountView)
608 |> render("accounts.json", %{users: users, as: :user})
614 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
615 local_only = params["local"] in [true, "True", "true", "1"]
618 [params["tag"], params["any"]]
622 |> Enum.map(&String.downcase(&1))
627 |> Enum.map(&String.downcase(&1))
632 |> Enum.map(&String.downcase(&1))
636 |> Map.put("type", "Create")
637 |> Map.put("local_only", local_only)
638 |> Map.put("blocking_user", user)
639 |> Map.put("muting_user", user)
640 |> Map.put("tag", tags)
641 |> Map.put("tag_all", tag_all)
642 |> Map.put("tag_reject", tag_reject)
643 |> ActivityPub.fetch_public_activities()
647 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
648 |> put_view(StatusView)
649 |> render("index.json", %{activities: activities, for: user, as: :activity})
652 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
653 with %User{} = user <- Repo.get(User, id),
654 {:ok, followers} <- User.get_followers(user) do
657 for_user && user.id == for_user.id -> followers
658 user.info.hide_followers -> []
663 |> put_view(AccountView)
664 |> render("accounts.json", %{users: followers, as: :user})
668 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
669 with %User{} = user <- Repo.get(User, id),
670 {:ok, followers} <- User.get_friends(user) do
673 for_user && user.id == for_user.id -> followers
674 user.info.hide_follows -> []
679 |> put_view(AccountView)
680 |> render("accounts.json", %{users: followers, as: :user})
684 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
685 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
687 |> put_view(AccountView)
688 |> render("accounts.json", %{users: follow_requests, as: :user})
692 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
693 with %User{} = follower <- Repo.get(User, id),
694 {:ok, follower} <- User.maybe_follow(follower, followed),
695 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
696 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
698 ActivityPub.accept(%{
699 to: [follower.ap_id],
701 object: follow_activity.data["id"],
705 |> put_view(AccountView)
706 |> render("relationship.json", %{user: followed, target: follower})
710 |> put_resp_content_type("application/json")
711 |> send_resp(403, Jason.encode!(%{"error" => message}))
715 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
716 with %User{} = follower <- Repo.get(User, id),
717 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
718 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
720 ActivityPub.reject(%{
721 to: [follower.ap_id],
723 object: follow_activity.data["id"],
727 |> put_view(AccountView)
728 |> render("relationship.json", %{user: followed, target: follower})
732 |> put_resp_content_type("application/json")
733 |> send_resp(403, Jason.encode!(%{"error" => message}))
737 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
738 with %User{} = followed <- Repo.get(User, id),
739 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
740 {:ok, _activity} <- ActivityPub.follow(follower, followed),
741 {:ok, follower, followed} <-
742 User.wait_and_refresh(
743 Config.get([:activitypub, :follow_handshake_timeout]),
748 |> put_view(AccountView)
749 |> render("relationship.json", %{user: follower, target: followed})
753 |> put_resp_content_type("application/json")
754 |> send_resp(403, Jason.encode!(%{"error" => message}))
758 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
759 with %User{} = followed <- Repo.get_by(User, nickname: uri),
760 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
761 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
763 |> put_view(AccountView)
764 |> render("account.json", %{user: followed, for: follower})
768 |> put_resp_content_type("application/json")
769 |> send_resp(403, Jason.encode!(%{"error" => message}))
773 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
774 with %User{} = followed <- Repo.get(User, id),
775 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
776 {:ok, follower, _} <- User.unfollow(follower, followed) do
778 |> put_view(AccountView)
779 |> render("relationship.json", %{user: follower, target: followed})
783 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
784 with %User{} = muted <- Repo.get(User, id),
785 {:ok, muter} <- User.mute(muter, muted) do
787 |> put_view(AccountView)
788 |> render("relationship.json", %{user: muter, target: muted})
792 |> put_resp_content_type("application/json")
793 |> send_resp(403, Jason.encode!(%{"error" => message}))
797 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
798 with %User{} = muted <- Repo.get(User, id),
799 {:ok, muter} <- User.unmute(muter, muted) do
801 |> put_view(AccountView)
802 |> render("relationship.json", %{user: muter, target: muted})
806 |> put_resp_content_type("application/json")
807 |> send_resp(403, Jason.encode!(%{"error" => message}))
811 def mutes(%{assigns: %{user: user}} = conn, _) do
812 with muted_accounts <- User.muted_users(user) do
813 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
818 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
819 with %User{} = blocked <- Repo.get(User, id),
820 {:ok, blocker} <- User.block(blocker, blocked),
821 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
823 |> put_view(AccountView)
824 |> render("relationship.json", %{user: blocker, target: blocked})
828 |> put_resp_content_type("application/json")
829 |> send_resp(403, Jason.encode!(%{"error" => message}))
833 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
834 with %User{} = blocked <- Repo.get(User, id),
835 {:ok, blocker} <- User.unblock(blocker, blocked),
836 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
838 |> put_view(AccountView)
839 |> render("relationship.json", %{user: blocker, target: blocked})
843 |> put_resp_content_type("application/json")
844 |> send_resp(403, Jason.encode!(%{"error" => message}))
848 def blocks(%{assigns: %{user: user}} = conn, _) do
849 with blocked_accounts <- User.blocked_users(user) do
850 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
855 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
856 json(conn, info.domain_blocks || [])
859 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
860 User.block_domain(blocker, domain)
864 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
865 User.unblock_domain(blocker, domain)
869 def status_search(user, query) do
871 if Regex.match?(~r/https?:/, query) do
872 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
873 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
874 true <- Visibility.visible_for_user?(activity, user) do
884 where: fragment("?->>'type' = 'Create'", a.data),
885 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
888 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
893 order_by: [desc: :id]
896 Repo.all(q) ++ fetched
899 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
900 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
902 statuses = status_search(user, query)
904 tags_path = Web.base_url() <> "/tag/"
910 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
911 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
912 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
915 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
917 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
924 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
925 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
927 statuses = status_search(user, query)
933 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
934 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
937 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
939 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
946 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
947 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
949 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
954 def favourites(%{assigns: %{user: user}} = conn, params) do
957 |> Map.put("type", "Create")
958 |> Map.put("favorited_by", user.ap_id)
959 |> Map.put("blocking_user", user)
960 |> ActivityPub.fetch_public_activities()
964 |> add_link_headers(:favourites, activities)
965 |> put_view(StatusView)
966 |> render("index.json", %{activities: activities, for: user, as: :activity})
969 def bookmarks(%{assigns: %{user: user}} = conn, _) do
970 user = Repo.get(User, user.id)
974 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
978 |> put_view(StatusView)
979 |> render("index.json", %{activities: activities, for: user, as: :activity})
982 def get_lists(%{assigns: %{user: user}} = conn, opts) do
983 lists = Pleroma.List.for_user(user, opts)
984 res = ListView.render("lists.json", lists: lists)
988 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
989 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
990 res = ListView.render("list.json", list: list)
996 |> json(%{error: "Record not found"})
1000 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1001 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1002 res = ListView.render("lists.json", lists: lists)
1006 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1007 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1008 {:ok, _list} <- Pleroma.List.delete(list) do
1016 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1017 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1018 res = ListView.render("list.json", list: list)
1023 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1025 |> Enum.each(fn account_id ->
1026 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1027 %User{} = followed <- Repo.get(User, account_id) do
1028 Pleroma.List.follow(list, followed)
1035 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1037 |> Enum.each(fn account_id ->
1038 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1039 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1040 Pleroma.List.unfollow(list, followed)
1047 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1048 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1049 {:ok, users} = Pleroma.List.get_following(list) do
1051 |> put_view(AccountView)
1052 |> render("accounts.json", %{users: users, as: :user})
1056 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1057 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1058 {:ok, list} <- Pleroma.List.rename(list, title) do
1059 res = ListView.render("list.json", list: list)
1067 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1068 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1071 |> Map.put("type", "Create")
1072 |> Map.put("blocking_user", user)
1073 |> Map.put("muting_user", user)
1075 # we must filter the following list for the user to avoid leaking statuses the user
1076 # does not actually have permission to see (for more info, peruse security issue #270).
1079 |> Enum.filter(fn x -> x in user.following end)
1080 |> ActivityPub.fetch_activities_bounded(following, params)
1084 |> put_view(StatusView)
1085 |> render("index.json", %{activities: activities, for: user, as: :activity})
1090 |> json(%{error: "Error."})
1094 def index(%{assigns: %{user: user}} = conn, _params) do
1097 |> get_session(:oauth_token)
1100 mastodon_emoji = mastodonized_emoji()
1102 limit = Config.get([:instance, :limit])
1105 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1107 flavour = get_user_flavour(user)
1112 streaming_api_base_url:
1113 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1114 access_token: token,
1116 domain: Pleroma.Web.Endpoint.host(),
1119 unfollow_modal: false,
1122 auto_play_gif: false,
1123 display_sensitive_media: false,
1124 reduce_motion: false,
1125 max_toot_chars: limit
1128 delete_others_notice: present?(user.info.is_moderator),
1129 admin: present?(user.info.is_admin)
1133 default_privacy: user.info.default_scope,
1134 default_sensitive: false
1136 media_attachments: %{
1137 accept_content_types: [
1153 user.info.settings ||
1183 push_subscription: nil,
1185 custom_emojis: mastodon_emoji,
1191 |> put_layout(false)
1192 |> put_view(MastodonView)
1193 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1196 |> redirect(to: "/web/login")
1200 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1201 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1203 with changeset <- Ecto.Changeset.change(user),
1204 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1205 {:ok, _user} <- User.update_and_set_cache(changeset) do
1210 |> put_resp_content_type("application/json")
1211 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1215 @supported_flavours ["glitch", "vanilla"]
1217 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1218 when flavour in @supported_flavours do
1219 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1221 with changeset <- Ecto.Changeset.change(user),
1222 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1223 {:ok, user} <- User.update_and_set_cache(changeset),
1224 flavour <- user.info.flavour do
1229 |> put_resp_content_type("application/json")
1230 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1234 def set_flavour(conn, _params) do
1237 |> json(%{error: "Unsupported flavour"})
1240 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1241 json(conn, get_user_flavour(user))
1244 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1248 defp get_user_flavour(_) do
1252 def login(conn, %{"code" => code}) do
1253 with {:ok, app} <- get_or_make_app(),
1254 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1255 {:ok, token} <- Token.exchange_token(app, auth) do
1257 |> put_session(:oauth_token, token.token)
1258 |> redirect(to: "/web/getting-started")
1262 def login(conn, _) do
1263 with {:ok, app} <- get_or_make_app() do
1268 response_type: "code",
1269 client_id: app.client_id,
1271 scope: Enum.join(app.scopes, " ")
1275 |> redirect(to: path)
1279 defp get_or_make_app() do
1280 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1281 scopes = ["read", "write", "follow", "push"]
1283 with %App{} = app <- Repo.get_by(App, find_attrs) do
1285 if app.scopes == scopes do
1289 |> Ecto.Changeset.change(%{scopes: scopes})
1297 App.register_changeset(
1299 Map.put(find_attrs, :scopes, scopes)
1306 def logout(conn, _) do
1309 |> redirect(to: "/")
1312 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1313 Logger.debug("Unimplemented, returning unmodified relationship")
1315 with %User{} = target <- Repo.get(User, id) do
1317 |> put_view(AccountView)
1318 |> render("relationship.json", %{user: user, target: target})
1322 def empty_array(conn, _) do
1323 Logger.debug("Unimplemented, returning an empty array")
1327 def empty_object(conn, _) do
1328 Logger.debug("Unimplemented, returning an empty object")
1332 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1333 actor = User.get_cached_by_ap_id(activity.data["actor"])
1334 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1335 mastodon_type = Activity.mastodon_notification_type(activity)
1339 type: mastodon_type,
1340 created_at: CommonAPI.Utils.to_masto_date(created_at),
1341 account: AccountView.render("account.json", %{user: actor, for: user})
1344 case mastodon_type do
1348 status: StatusView.render("status.json", %{activity: activity, for: user})
1354 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1360 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1371 def get_filters(%{assigns: %{user: user}} = conn, _) do
1372 filters = Filter.get_filters(user)
1373 res = FilterView.render("filters.json", filters: filters)
1378 %{assigns: %{user: user}} = conn,
1379 %{"phrase" => phrase, "context" => context} = params
1385 hide: Map.get(params, "irreversible", nil),
1386 whole_word: Map.get(params, "boolean", true)
1390 {:ok, response} = Filter.create(query)
1391 res = FilterView.render("filter.json", filter: response)
1395 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1396 filter = Filter.get(filter_id, user)
1397 res = FilterView.render("filter.json", filter: filter)
1402 %{assigns: %{user: user}} = conn,
1403 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1407 filter_id: filter_id,
1410 hide: Map.get(params, "irreversible", nil),
1411 whole_word: Map.get(params, "boolean", true)
1415 {:ok, response} = Filter.update(query)
1416 res = FilterView.render("filter.json", filter: response)
1420 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1423 filter_id: filter_id
1426 {:ok, _} = Filter.delete(query)
1430 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1431 true = Push.enabled()
1432 Subscription.delete_if_exists(user, token)
1433 {:ok, subscription} = Subscription.create(user, token, params)
1434 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1438 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1439 true = Push.enabled()
1440 subscription = Subscription.get(user, token)
1441 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1445 def update_push_subscription(
1446 %{assigns: %{user: user, token: token}} = conn,
1449 true = Push.enabled()
1450 {:ok, subscription} = Subscription.update(user, token, params)
1451 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1455 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1456 true = Push.enabled()
1457 {:ok, _response} = Subscription.delete(user, token)
1461 def errors(conn, _) do
1464 |> json("Something went wrong")
1467 def suggestions(%{assigns: %{user: user}} = conn, _) do
1468 suggestions = Config.get(:suggestions)
1470 if Keyword.get(suggestions, :enabled, false) do
1471 api = Keyword.get(suggestions, :third_party_engine, "")
1472 timeout = Keyword.get(suggestions, :timeout, 5000)
1473 limit = Keyword.get(suggestions, :limit, 23)
1475 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1477 user = user.nickname
1481 |> String.replace("{{host}}", host)
1482 |> String.replace("{{user}}", user)
1484 with {:ok, %{status: 200, body: body}} <-
1490 recv_timeout: timeout,
1494 {:ok, data} <- Jason.decode(body) do
1497 |> Enum.slice(0, limit)
1502 case User.get_or_fetch(x["acct"]) do
1509 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1512 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1518 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1525 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1526 with %Activity{} = activity <- Repo.get(Activity, status_id),
1527 true <- Visibility.visible_for_user?(activity, user) do
1531 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1541 def reports(%{assigns: %{user: user}} = conn, params) do
1542 case CommonAPI.report(user, params) do
1545 |> put_view(ReportView)
1546 |> try_render("report.json", %{activity: activity})
1550 |> put_status(:bad_request)
1551 |> json(%{error: err})
1555 def try_render(conn, target, params)
1556 when is_binary(target) do
1557 res = render(conn, target, params)
1562 |> json(%{error: "Can't display this activity"})
1568 def try_render(conn, _, _) do
1571 |> json(%{error: "Can't display this activity"})
1574 defp present?(nil), do: false
1575 defp present?(false), do: false
1576 defp present?(_), do: true