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],
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],
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}))
1063 flavour = get_user_flavour(user)
1068 streaming_api_base_url:
1069 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1070 access_token: token,
1072 domain: Pleroma.Web.Endpoint.host(),
1075 unfollow_modal: false,
1078 auto_play_gif: false,
1079 display_sensitive_media: false,
1080 reduce_motion: false,
1081 max_toot_chars: limit
1084 delete_others_notice: present?(user.info.is_moderator),
1085 admin: present?(user.info.is_admin)
1089 default_privacy: user.info.default_scope,
1090 default_sensitive: false
1092 media_attachments: %{
1093 accept_content_types: [
1109 user.info.settings ||
1139 push_subscription: nil,
1141 custom_emojis: mastodon_emoji,
1147 |> put_layout(false)
1148 |> put_view(MastodonView)
1149 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1152 |> redirect(to: "/web/login")
1156 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1157 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1159 with changeset <- Ecto.Changeset.change(user),
1160 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1161 {:ok, _user} <- User.update_and_set_cache(changeset) do
1166 |> put_resp_content_type("application/json")
1167 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1171 @supported_flavours ["glitch", "vanilla"]
1173 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1174 when flavour in @supported_flavours do
1175 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1177 with changeset <- Ecto.Changeset.change(user),
1178 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1179 {:ok, user} <- User.update_and_set_cache(changeset),
1180 flavour <- user.info.flavour do
1185 |> put_resp_content_type("application/json")
1186 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1190 def set_flavour(conn, _params) do
1193 |> json(%{error: "Unsupported flavour"})
1196 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1197 json(conn, get_user_flavour(user))
1200 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1204 defp get_user_flavour(_) do
1208 def login(conn, %{"code" => code}) do
1209 with {:ok, app} <- get_or_make_app(),
1210 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1211 {:ok, token} <- Token.exchange_token(app, auth) do
1213 |> put_session(:oauth_token, token.token)
1214 |> redirect(to: "/web/getting-started")
1218 def login(conn, _) do
1219 with {:ok, app} <- get_or_make_app() do
1224 response_type: "code",
1225 client_id: app.client_id,
1227 scope: Enum.join(app.scopes, " ")
1231 |> redirect(to: path)
1235 defp get_or_make_app() do
1236 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1238 with %App{} = app <- Repo.get_by(App, find_attrs) do
1243 App.register_changeset(
1245 Map.put(find_attrs, :scopes, ["read", "write", "follow"])
1252 def logout(conn, _) do
1255 |> redirect(to: "/")
1258 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1259 Logger.debug("Unimplemented, returning unmodified relationship")
1261 with %User{} = target <- Repo.get(User, id) do
1263 |> put_view(AccountView)
1264 |> render("relationship.json", %{user: user, target: target})
1268 def empty_array(conn, _) do
1269 Logger.debug("Unimplemented, returning an empty array")
1273 def empty_object(conn, _) do
1274 Logger.debug("Unimplemented, returning an empty object")
1278 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1279 actor = User.get_cached_by_ap_id(activity.data["actor"])
1280 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1281 mastodon_type = Activity.mastodon_notification_type(activity)
1285 type: mastodon_type,
1286 created_at: CommonAPI.Utils.to_masto_date(created_at),
1287 account: AccountView.render("account.json", %{user: actor, for: user})
1290 case mastodon_type do
1294 status: StatusView.render("status.json", %{activity: activity, for: user})
1300 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1306 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1317 def get_filters(%{assigns: %{user: user}} = conn, _) do
1318 filters = Filter.get_filters(user)
1319 res = FilterView.render("filters.json", filters: filters)
1324 %{assigns: %{user: user}} = conn,
1325 %{"phrase" => phrase, "context" => context} = params
1331 hide: Map.get(params, "irreversible", nil),
1332 whole_word: Map.get(params, "boolean", true)
1336 {:ok, response} = Filter.create(query)
1337 res = FilterView.render("filter.json", filter: response)
1341 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1342 filter = Filter.get(filter_id, user)
1343 res = FilterView.render("filter.json", filter: filter)
1348 %{assigns: %{user: user}} = conn,
1349 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1353 filter_id: filter_id,
1356 hide: Map.get(params, "irreversible", nil),
1357 whole_word: Map.get(params, "boolean", true)
1361 {:ok, response} = Filter.update(query)
1362 res = FilterView.render("filter.json", filter: response)
1366 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1369 filter_id: filter_id
1372 {:ok, _} = Filter.delete(query)
1376 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1377 true = Push.enabled()
1378 Subscription.delete_if_exists(user, token)
1379 {:ok, subscription} = Subscription.create(user, token, params)
1380 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1384 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1385 true = Push.enabled()
1386 subscription = Subscription.get(user, token)
1387 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1391 def update_push_subscription(
1392 %{assigns: %{user: user, token: token}} = conn,
1395 true = Push.enabled()
1396 {:ok, subscription} = Subscription.update(user, token, params)
1397 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1401 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1402 true = Push.enabled()
1403 {:ok, _response} = Subscription.delete(user, token)
1407 def errors(conn, _) do
1410 |> json("Something went wrong")
1413 def suggestions(%{assigns: %{user: user}} = conn, _) do
1414 suggestions = Config.get(:suggestions)
1416 if Keyword.get(suggestions, :enabled, false) do
1417 api = Keyword.get(suggestions, :third_party_engine, "")
1418 timeout = Keyword.get(suggestions, :timeout, 5000)
1419 limit = Keyword.get(suggestions, :limit, 23)
1421 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1423 user = user.nickname
1427 |> String.replace("{{host}}", host)
1428 |> String.replace("{{user}}", user)
1430 with {:ok, %{status: 200, body: body}} <-
1436 recv_timeout: timeout,
1440 {:ok, data} <- Jason.decode(body) do
1443 |> Enum.slice(0, limit)
1448 case User.get_or_fetch(x["acct"]) do
1455 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1458 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1464 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1471 def status_card(conn, %{"id" => status_id}) do
1472 with %Activity{} = activity <- Repo.get(Activity, status_id),
1473 true <- ActivityPub.is_public?(activity) do
1477 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1487 def try_render(conn, target, params)
1488 when is_binary(target) do
1489 res = render(conn, target, params)
1494 |> json(%{error: "Can't display this activity"})
1500 def try_render(conn, _, _) do
1503 |> json(%{error: "Can't display this activity"})
1506 defp present?(nil), do: false
1507 defp present?(false), do: false
1508 defp present?(_), do: true