1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
10 alias Pleroma.Notification
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MediaProxy
18 alias Pleroma.Web.Push
19 alias Push.Subscription
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.PushSubscriptionView
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Utils
29 alias Pleroma.Web.OAuth.App
30 alias Pleroma.Web.OAuth.Authorization
31 alias Pleroma.Web.OAuth.Token
33 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
38 @httpoison Application.get_env(:pleroma, :httpoison)
39 @local_mastodon_name "Mastodon-Local"
41 action_fallback(:errors)
43 def create_app(conn, params) do
44 scopes = oauth_scopes(params, ["read"])
48 |> Map.drop(["scope", "scopes"])
49 |> Map.put("scopes", scopes)
51 with cs <- App.register_changeset(%App{}, app_attrs),
52 false <- cs.changes[:client_name] == @local_mastodon_name,
53 {:ok, app} <- Repo.insert(cs) do
55 id: app.id |> to_string,
56 name: app.client_name,
57 client_id: app.client_id,
58 client_secret: app.client_secret,
59 redirect_uri: app.redirect_uris,
72 value_function \\ fn x -> {:ok, x} end
74 if Map.has_key?(params, params_field) do
75 case value_function.(params[params_field]) do
76 {:ok, new_value} -> Map.put(map, map_field, new_value)
84 def update_credentials(%{assigns: %{user: user}} = conn, params) do
89 |> add_if_present(params, "display_name", :name)
90 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
91 |> add_if_present(params, "avatar", :avatar, fn value ->
92 with %Plug.Upload{} <- value,
93 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
102 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
103 |> add_if_present(params, "header", :banner, fn value ->
104 with %Plug.Upload{} <- value,
105 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
112 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
114 with changeset <- User.update_changeset(user, user_params),
115 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
116 {:ok, user} <- User.update_and_set_cache(changeset) do
117 if original_user != user do
118 CommonAPI.update(user)
121 json(conn, AccountView.render("account.json", %{user: user, for: user}))
126 |> json(%{error: "Invalid request"})
130 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131 account = AccountView.render("account.json", %{user: user, for: user})
135 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
136 with %User{} = user <- Repo.get(User, id),
137 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
138 account = AccountView.render("account.json", %{user: user, for: for_user})
144 |> json(%{error: "Can't find user"})
148 @mastodon_api_level "2.5.0"
150 def masto_instance(conn, _params) do
151 instance = Config.get(:instance)
155 title: Keyword.get(instance, :name),
156 description: Keyword.get(instance, :description),
157 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
158 email: Keyword.get(instance, :email),
160 streaming_api: Pleroma.Web.Endpoint.websocket_url()
162 stats: Stats.get_stats(),
163 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
164 max_toot_chars: Keyword.get(instance, :limit)
170 def peers(conn, _params) do
171 json(conn, Stats.get_peers())
174 defp mastodonized_emoji do
175 Pleroma.Emoji.get_all()
176 |> Enum.map(fn {shortcode, relative_url} ->
177 url = to_string(URI.merge(Web.base_url(), relative_url))
180 "shortcode" => shortcode,
182 "visible_in_picker" => true,
188 def custom_emojis(conn, _params) do
189 mastodon_emoji = mastodonized_emoji()
190 json(conn, mastodon_emoji)
193 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
194 last = List.last(activities)
195 first = List.first(activities)
201 {next_url, prev_url} =
205 Pleroma.Web.Endpoint,
208 Map.merge(params, %{max_id: min})
211 Pleroma.Web.Endpoint,
214 Map.merge(params, %{since_id: max})
220 Pleroma.Web.Endpoint,
222 Map.merge(params, %{max_id: min})
225 Pleroma.Web.Endpoint,
227 Map.merge(params, %{since_id: max})
233 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
239 def home_timeline(%{assigns: %{user: user}} = conn, params) do
242 |> Map.put("type", ["Create", "Announce"])
243 |> Map.put("blocking_user", user)
244 |> Map.put("user", user)
247 [user.ap_id | user.following]
248 |> ActivityPub.fetch_activities(params)
249 |> ActivityPub.contain_timeline(user)
253 |> add_link_headers(:home_timeline, activities)
254 |> put_view(StatusView)
255 |> render("index.json", %{activities: activities, for: user, as: :activity})
258 def public_timeline(%{assigns: %{user: user}} = conn, params) do
259 local_only = params["local"] in [true, "True", "true", "1"]
263 |> Map.put("type", ["Create", "Announce"])
264 |> Map.put("local_only", local_only)
265 |> Map.put("blocking_user", user)
266 |> ActivityPub.fetch_public_activities()
270 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
271 |> put_view(StatusView)
272 |> render("index.json", %{activities: activities, for: user, as: :activity})
275 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
276 with %User{} = user <- Repo.get(User, params["id"]) do
277 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
280 |> add_link_headers(:user_statuses, activities, params["id"])
281 |> put_view(StatusView)
282 |> render("index.json", %{
283 activities: activities,
290 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
292 ActivityPub.fetch_activities_query(
294 Map.merge(params, %{"type" => "Create", visibility: "direct"})
297 activities = Repo.all(query)
300 |> add_link_headers(:dm_timeline, activities)
301 |> put_view(StatusView)
302 |> render("index.json", %{activities: activities, for: user, as: :activity})
305 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
306 with %Activity{} = activity <- Repo.get(Activity, id),
307 true <- ActivityPub.visible_for_user?(activity, user) do
309 |> put_view(StatusView)
310 |> try_render("status.json", %{activity: activity, for: user})
314 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
315 with %Activity{} = activity <- Repo.get(Activity, id),
317 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
318 "blocking_user" => user,
322 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
324 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
325 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
331 activities: grouped_activities[true] || [],
335 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
340 activities: grouped_activities[false] || [],
344 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
351 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
352 when length(media_ids) > 0 do
355 |> Map.put("status", ".")
357 post_status(conn, params)
360 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
363 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
366 case get_req_header(conn, "idempotency-key") do
368 _ -> Ecto.UUID.generate()
372 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
375 |> put_view(StatusView)
376 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
379 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
380 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
386 |> json(%{error: "Can't delete this post"})
390 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
391 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
393 |> put_view(StatusView)
394 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
398 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
399 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
400 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
402 |> put_view(StatusView)
403 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
407 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
408 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
409 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
411 |> put_view(StatusView)
412 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
416 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
417 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
418 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
420 |> put_view(StatusView)
421 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
425 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
426 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
428 |> put_view(StatusView)
429 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
433 |> put_resp_content_type("application/json")
434 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
438 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
439 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
441 |> put_view(StatusView)
442 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
446 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Repo.get(Activity, id),
448 %User{} = user <- User.get_by_nickname(user.nickname),
449 true <- ActivityPub.visible_for_user?(activity, user),
450 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
452 |> put_view(StatusView)
453 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
457 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
458 with %Activity{} = activity <- Repo.get(Activity, id),
459 %User{} = user <- User.get_by_nickname(user.nickname),
460 true <- ActivityPub.visible_for_user?(activity, user),
461 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
463 |> put_view(StatusView)
464 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
468 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
469 activity = Activity.get_by_id(id)
471 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 |> put_resp_content_type("application/json")
479 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
483 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
484 activity = Activity.get_by_id(id)
486 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
488 |> put_view(StatusView)
489 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
493 def notifications(%{assigns: %{user: user}} = conn, params) do
494 notifications = Notification.for_user(user, params)
498 |> Enum.map(fn x -> render_notification(user, x) end)
502 |> add_link_headers(:notifications, notifications)
506 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
507 with {:ok, notification} <- Notification.get(user, id) do
508 json(conn, render_notification(user, notification))
512 |> put_resp_content_type("application/json")
513 |> send_resp(403, Jason.encode!(%{"error" => reason}))
517 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
518 Notification.clear(user)
522 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
523 with {:ok, _notif} <- Notification.dismiss(user, id) do
528 |> put_resp_content_type("application/json")
529 |> send_resp(403, Jason.encode!(%{"error" => reason}))
533 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
535 q = from(u in User, where: u.id in ^id)
536 targets = Repo.all(q)
539 |> put_view(AccountView)
540 |> render("relationships.json", %{user: user, targets: targets})
543 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
544 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
546 def update_media(%{assigns: %{user: user}} = conn, data) do
547 with %Object{} = object <- Repo.get(Object, data["id"]),
548 true <- Object.authorize_mutation(object, user),
549 true <- is_binary(data["description"]),
550 description <- data["description"] do
551 new_data = %{object.data | "name" => description}
555 |> Object.change(%{data: new_data})
558 attachment_data = Map.put(new_data, "id", object.id)
561 |> put_view(StatusView)
562 |> render("attachment.json", %{attachment: attachment_data})
566 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
567 with {:ok, object} <-
570 actor: User.ap_id(user),
571 description: Map.get(data, "description")
573 attachment_data = Map.put(object.data, "id", object.id)
576 |> put_view(StatusView)
577 |> render("attachment.json", %{attachment: attachment_data})
581 def favourited_by(conn, %{"id" => id}) do
582 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
583 q = from(u in User, where: u.ap_id in ^likes)
587 |> put_view(AccountView)
588 |> render(AccountView, "accounts.json", %{users: users, as: :user})
594 def reblogged_by(conn, %{"id" => id}) do
595 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
596 q = from(u in User, where: u.ap_id in ^announces)
600 |> put_view(AccountView)
601 |> render("accounts.json", %{users: users, as: :user})
607 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
608 local_only = params["local"] in [true, "True", "true", "1"]
611 [params["tag"], params["any"]]
615 |> Enum.map(&String.downcase(&1))
620 |> Enum.map(&String.downcase(&1))
625 |> Enum.map(&String.downcase(&1))
629 |> Map.put("type", "Create")
630 |> Map.put("local_only", local_only)
631 |> Map.put("blocking_user", user)
632 |> Map.put("tag", tags)
633 |> Map.put("tag_all", tag_all)
634 |> Map.put("tag_reject", tag_reject)
635 |> ActivityPub.fetch_public_activities()
639 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
640 |> put_view(StatusView)
641 |> render("index.json", %{activities: activities, for: user, as: :activity})
644 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
645 with %User{} = user <- Repo.get(User, id),
646 {:ok, followers} <- User.get_followers(user) do
649 for_user && user.id == for_user.id -> followers
650 user.info.hide_followers -> []
655 |> put_view(AccountView)
656 |> render("accounts.json", %{users: followers, as: :user})
660 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
661 with %User{} = user <- Repo.get(User, id),
662 {:ok, followers} <- User.get_friends(user) do
665 for_user && user.id == for_user.id -> followers
666 user.info.hide_follows -> []
671 |> put_view(AccountView)
672 |> render("accounts.json", %{users: followers, as: :user})
676 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
677 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
679 |> put_view(AccountView)
680 |> render("accounts.json", %{users: follow_requests, as: :user})
684 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
685 with %User{} = follower <- Repo.get(User, id),
686 {:ok, follower} <- User.maybe_follow(follower, followed),
687 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
688 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
690 ActivityPub.accept(%{
691 to: [follower.ap_id],
692 actor: followed.ap_id,
693 object: follow_activity.data["id"],
697 |> put_view(AccountView)
698 |> render("relationship.json", %{user: followed, target: follower})
702 |> put_resp_content_type("application/json")
703 |> send_resp(403, Jason.encode!(%{"error" => message}))
707 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
708 with %User{} = follower <- Repo.get(User, id),
709 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
710 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
712 ActivityPub.reject(%{
713 to: [follower.ap_id],
714 actor: followed.ap_id,
715 object: follow_activity.data["id"],
719 |> put_view(AccountView)
720 |> render("relationship.json", %{user: followed, target: follower})
724 |> put_resp_content_type("application/json")
725 |> send_resp(403, Jason.encode!(%{"error" => message}))
729 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
730 with %User{} = followed <- Repo.get(User, id),
731 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
732 {:ok, _activity} <- ActivityPub.follow(follower, followed),
733 {:ok, follower, followed} <-
734 User.wait_and_refresh(
735 Config.get([:activitypub, :follow_handshake_timeout]),
740 |> put_view(AccountView)
741 |> render("relationship.json", %{user: follower, target: followed})
745 |> put_resp_content_type("application/json")
746 |> send_resp(403, Jason.encode!(%{"error" => message}))
750 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
751 with %User{} = followed <- Repo.get_by(User, nickname: uri),
752 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
753 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
755 |> put_view(AccountView)
756 |> render("account.json", %{user: followed, for: follower})
760 |> put_resp_content_type("application/json")
761 |> send_resp(403, Jason.encode!(%{"error" => message}))
765 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
766 with %User{} = followed <- Repo.get(User, id),
767 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
768 {:ok, follower, _} <- User.unfollow(follower, followed) do
770 |> put_view(AccountView)
771 |> render("relationship.json", %{user: follower, target: followed})
775 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
776 with %User{} = blocked <- Repo.get(User, id),
777 {:ok, blocker} <- User.block(blocker, blocked),
778 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
780 |> put_view(AccountView)
781 |> render("relationship.json", %{user: blocker, target: blocked})
785 |> put_resp_content_type("application/json")
786 |> send_resp(403, Jason.encode!(%{"error" => message}))
790 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
791 with %User{} = blocked <- Repo.get(User, id),
792 {:ok, blocker} <- User.unblock(blocker, blocked),
793 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
795 |> put_view(AccountView)
796 |> render("relationship.json", %{user: blocker, target: blocked})
800 |> put_resp_content_type("application/json")
801 |> send_resp(403, Jason.encode!(%{"error" => message}))
805 def blocks(%{assigns: %{user: user}} = conn, _) do
806 with blocked_accounts <- User.blocked_users(user) do
807 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
812 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
813 json(conn, info.domain_blocks || [])
816 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
817 User.block_domain(blocker, domain)
821 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
822 User.unblock_domain(blocker, domain)
826 def status_search(user, query) do
828 if Regex.match?(~r/https?:/, query) do
829 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
830 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
831 true <- ActivityPub.visible_for_user?(activity, user) do
841 where: fragment("?->>'type' = 'Create'", a.data),
842 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
845 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
850 order_by: [desc: :id]
853 Repo.all(q) ++ fetched
856 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
857 accounts = User.search(query, params["resolve"] == "true", user)
859 statuses = status_search(user, query)
861 tags_path = Web.base_url() <> "/tag/"
867 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
868 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
869 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
872 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
874 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
881 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
882 accounts = User.search(query, params["resolve"] == "true", user)
884 statuses = status_search(user, query)
890 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
891 |> Enum.map(fn tag -> String.slice(tag, 1..-1) 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 account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
904 accounts = User.search(query, params["resolve"] == "true", user)
906 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
911 def favourites(%{assigns: %{user: user}} = conn, params) do
914 |> Map.put("type", "Create")
915 |> Map.put("favorited_by", user.ap_id)
916 |> Map.put("blocking_user", user)
917 |> ActivityPub.fetch_public_activities()
921 |> add_link_headers(:favourites, activities)
922 |> put_view(StatusView)
923 |> render("index.json", %{activities: activities, for: user, as: :activity})
926 def bookmarks(%{assigns: %{user: user}} = conn, _) do
927 user = Repo.get(User, user.id)
931 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
935 |> put_view(StatusView)
936 |> render("index.json", %{activities: activities, for: user, as: :activity})
939 def get_lists(%{assigns: %{user: user}} = conn, opts) do
940 lists = Pleroma.List.for_user(user, opts)
941 res = ListView.render("lists.json", lists: lists)
945 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
946 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
947 res = ListView.render("list.json", list: list)
953 |> json(%{error: "Record not found"})
957 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
958 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
959 res = ListView.render("lists.json", lists: lists)
963 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
964 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
965 {:ok, _list} <- Pleroma.List.delete(list) do
973 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
974 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
975 res = ListView.render("list.json", list: list)
980 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
982 |> Enum.each(fn account_id ->
983 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
984 %User{} = followed <- Repo.get(User, account_id) do
985 Pleroma.List.follow(list, followed)
992 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
994 |> Enum.each(fn account_id ->
995 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
996 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
997 Pleroma.List.unfollow(list, followed)
1004 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1005 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1006 {:ok, users} = Pleroma.List.get_following(list) do
1008 |> put_view(AccountView)
1009 |> render("accounts.json", %{users: users, as: :user})
1013 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1014 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1015 {:ok, list} <- Pleroma.List.rename(list, title) do
1016 res = ListView.render("list.json", list: list)
1024 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1025 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1028 |> Map.put("type", "Create")
1029 |> Map.put("blocking_user", user)
1031 # we must filter the following list for the user to avoid leaking statuses the user
1032 # does not actually have permission to see (for more info, peruse security issue #270).
1035 |> Enum.filter(fn x -> x in user.following end)
1036 |> ActivityPub.fetch_activities_bounded(following, params)
1040 |> put_view(StatusView)
1041 |> render("index.json", %{activities: activities, for: user, as: :activity})
1046 |> json(%{error: "Error."})
1050 def index(%{assigns: %{user: user}} = conn, _params) do
1053 |> get_session(:oauth_token)
1056 mastodon_emoji = mastodonized_emoji()
1058 limit = Config.get([:instance, :limit])
1061 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1066 streaming_api_base_url:
1067 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1068 access_token: token,
1070 domain: Pleroma.Web.Endpoint.host(),
1073 unfollow_modal: false,
1076 auto_play_gif: false,
1077 display_sensitive_media: false,
1078 reduce_motion: false,
1079 max_toot_chars: limit
1082 delete_others_notice: present?(user.info.is_moderator),
1083 admin: present?(user.info.is_admin)
1087 default_privacy: user.info.default_scope,
1088 default_sensitive: false
1090 media_attachments: %{
1091 accept_content_types: [
1107 user.info.settings ||
1137 push_subscription: nil,
1139 custom_emojis: mastodon_emoji,
1145 |> put_layout(false)
1146 |> put_view(MastodonView)
1147 |> render("index.html", %{initial_state: initial_state})
1150 |> redirect(to: "/web/login")
1154 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1155 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1157 with changeset <- Ecto.Changeset.change(user),
1158 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1159 {:ok, _user} <- User.update_and_set_cache(changeset) do
1164 |> put_resp_content_type("application/json")
1165 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1169 def login(conn, %{"code" => code}) do
1170 with {:ok, app} <- get_or_make_app(),
1171 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1172 {:ok, token} <- Token.exchange_token(app, auth) do
1174 |> put_session(:oauth_token, token.token)
1175 |> redirect(to: "/web/getting-started")
1179 def login(conn, _) do
1180 with {:ok, app} <- get_or_make_app() do
1185 response_type: "code",
1186 client_id: app.client_id,
1188 scope: Enum.join(app.scopes, " ")
1192 |> redirect(to: path)
1196 defp get_or_make_app() do
1197 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1199 with %App{} = app <- Repo.get_by(App, find_attrs) do
1204 App.register_changeset(
1206 Map.put(find_attrs, :scopes, ["read", "write", "follow"])
1213 def logout(conn, _) do
1216 |> redirect(to: "/")
1219 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1220 Logger.debug("Unimplemented, returning unmodified relationship")
1222 with %User{} = target <- Repo.get(User, id) do
1224 |> put_view(AccountView)
1225 |> render("relationship.json", %{user: user, target: target})
1229 def empty_array(conn, _) do
1230 Logger.debug("Unimplemented, returning an empty array")
1234 def empty_object(conn, _) do
1235 Logger.debug("Unimplemented, returning an empty object")
1239 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1240 actor = User.get_cached_by_ap_id(activity.data["actor"])
1241 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1242 mastodon_type = Activity.mastodon_notification_type(activity)
1246 type: mastodon_type,
1247 created_at: CommonAPI.Utils.to_masto_date(created_at),
1248 account: AccountView.render("account.json", %{user: actor, for: user})
1251 case mastodon_type do
1255 status: StatusView.render("status.json", %{activity: activity, for: user})
1261 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1267 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1278 def get_filters(%{assigns: %{user: user}} = conn, _) do
1279 filters = Filter.get_filters(user)
1280 res = FilterView.render("filters.json", filters: filters)
1285 %{assigns: %{user: user}} = conn,
1286 %{"phrase" => phrase, "context" => context} = params
1292 hide: Map.get(params, "irreversible", nil),
1293 whole_word: Map.get(params, "boolean", true)
1297 {:ok, response} = Filter.create(query)
1298 res = FilterView.render("filter.json", filter: response)
1302 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1303 filter = Filter.get(filter_id, user)
1304 res = FilterView.render("filter.json", filter: filter)
1309 %{assigns: %{user: user}} = conn,
1310 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1314 filter_id: filter_id,
1317 hide: Map.get(params, "irreversible", nil),
1318 whole_word: Map.get(params, "boolean", true)
1322 {:ok, response} = Filter.update(query)
1323 res = FilterView.render("filter.json", filter: response)
1327 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1330 filter_id: filter_id
1333 {:ok, _} = Filter.delete(query)
1337 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1338 true = Push.enabled()
1339 Subscription.delete_if_exists(user, token)
1340 {:ok, subscription} = Subscription.create(user, token, params)
1341 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1345 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1346 true = Push.enabled()
1347 subscription = Subscription.get(user, token)
1348 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1352 def update_push_subscription(
1353 %{assigns: %{user: user, token: token}} = conn,
1356 true = Push.enabled()
1357 {:ok, subscription} = Subscription.update(user, token, params)
1358 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1362 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1363 true = Push.enabled()
1364 {:ok, _response} = Subscription.delete(user, token)
1368 def errors(conn, _) do
1371 |> json("Something went wrong")
1374 def suggestions(%{assigns: %{user: user}} = conn, _) do
1375 suggestions = Config.get(:suggestions)
1377 if Keyword.get(suggestions, :enabled, false) do
1378 api = Keyword.get(suggestions, :third_party_engine, "")
1379 timeout = Keyword.get(suggestions, :timeout, 5000)
1380 limit = Keyword.get(suggestions, :limit, 23)
1382 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1384 user = user.nickname
1388 |> String.replace("{{host}}", host)
1389 |> String.replace("{{user}}", user)
1391 with {:ok, %{status: 200, body: body}} <-
1397 recv_timeout: timeout,
1401 {:ok, data} <- Jason.decode(body) do
1404 |> Enum.slice(0, limit)
1409 case User.get_or_fetch(x["acct"]) do
1416 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1419 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1425 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1432 def status_card(conn, %{"id" => status_id}) do
1433 with %Activity{} = activity <- Repo.get(Activity, status_id),
1434 true <- ActivityPub.is_public?(activity) do
1438 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1448 def try_render(conn, target, params)
1449 when is_binary(target) do
1450 res = render(conn, target, params)
1455 |> json(%{error: "Can't display this activity"})
1461 def try_render(conn, _, _) do
1464 |> json(%{error: "Can't display this activity"})
1467 defp present?(nil), do: false
1468 defp present?(false), do: false
1469 defp present?(_), do: true