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
36 @httpoison Application.get_env(:pleroma, :httpoison)
37 @local_mastodon_name "Mastodon-Local"
39 action_fallback(:errors)
41 def create_app(conn, params) do
42 with cs <- App.register_changeset(%App{}, params),
43 false <- cs.changes[:client_name] == @local_mastodon_name,
44 {:ok, app} <- Repo.insert(cs) do
46 id: app.id |> to_string,
47 name: app.client_name,
48 client_id: app.client_id,
49 client_secret: app.client_secret,
50 redirect_uri: app.redirect_uris,
63 value_function \\ fn x -> {:ok, x} end
65 if Map.has_key?(params, params_field) do
66 case value_function.(params[params_field]) do
67 {:ok, new_value} -> Map.put(map, map_field, new_value)
75 def update_credentials(%{assigns: %{user: user}} = conn, params) do
80 |> add_if_present(params, "display_name", :name)
81 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
82 |> add_if_present(params, "avatar", :avatar, fn value ->
83 with %Plug.Upload{} <- value,
84 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
93 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
94 |> add_if_present(params, "header", :banner, fn value ->
95 with %Plug.Upload{} <- value,
96 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
103 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
105 with changeset <- User.update_changeset(user, user_params),
106 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
107 {:ok, user} <- User.update_and_set_cache(changeset) do
108 if original_user != user do
109 CommonAPI.update(user)
112 json(conn, AccountView.render("account.json", %{user: user, for: user}))
117 |> json(%{error: "Invalid request"})
121 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
122 account = AccountView.render("account.json", %{user: user, for: user})
126 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
127 with %User{} = user <- Repo.get(User, id),
128 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
129 account = AccountView.render("account.json", %{user: user, for: for_user})
135 |> json(%{error: "Can't find user"})
139 @mastodon_api_level "2.5.0"
141 def masto_instance(conn, _params) do
142 instance = Config.get(:instance)
146 title: Keyword.get(instance, :name),
147 description: Keyword.get(instance, :description),
148 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
149 email: Keyword.get(instance, :email),
151 streaming_api: Pleroma.Web.Endpoint.websocket_url()
153 stats: Stats.get_stats(),
154 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
155 max_toot_chars: Keyword.get(instance, :limit)
161 def peers(conn, _params) do
162 json(conn, Stats.get_peers())
165 defp mastodonized_emoji do
166 Pleroma.Emoji.get_all()
167 |> Enum.map(fn {shortcode, relative_url} ->
168 url = to_string(URI.merge(Web.base_url(), relative_url))
171 "shortcode" => shortcode,
173 "visible_in_picker" => true,
179 def custom_emojis(conn, _params) do
180 mastodon_emoji = mastodonized_emoji()
181 json(conn, mastodon_emoji)
184 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
185 last = List.last(activities)
186 first = List.first(activities)
192 {next_url, prev_url} =
196 Pleroma.Web.Endpoint,
199 Map.merge(params, %{max_id: min})
202 Pleroma.Web.Endpoint,
205 Map.merge(params, %{since_id: max})
211 Pleroma.Web.Endpoint,
213 Map.merge(params, %{max_id: min})
216 Pleroma.Web.Endpoint,
218 Map.merge(params, %{since_id: max})
224 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
230 def home_timeline(%{assigns: %{user: user}} = conn, params) do
233 |> Map.put("type", ["Create", "Announce"])
234 |> Map.put("blocking_user", user)
235 |> Map.put("muting_user", user)
236 |> Map.put("user", user)
239 [user.ap_id | user.following]
240 |> ActivityPub.fetch_activities(params)
241 |> ActivityPub.contain_timeline(user)
245 |> add_link_headers(:home_timeline, activities)
246 |> put_view(StatusView)
247 |> render("index.json", %{activities: activities, for: user, as: :activity})
250 def public_timeline(%{assigns: %{user: user}} = conn, params) do
251 local_only = params["local"] in [true, "True", "true", "1"]
255 |> Map.put("type", ["Create", "Announce"])
256 |> Map.put("local_only", local_only)
257 |> Map.put("blocking_user", user)
258 |> Map.put("muting_user", user)
259 |> ActivityPub.fetch_public_activities()
263 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
264 |> put_view(StatusView)
265 |> render("index.json", %{activities: activities, for: user, as: :activity})
268 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
269 with %User{} = user <- Repo.get(User, params["id"]) do
270 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
273 |> add_link_headers(:user_statuses, activities, params["id"])
274 |> put_view(StatusView)
275 |> render("index.json", %{
276 activities: activities,
283 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
285 ActivityPub.fetch_activities_query(
287 Map.merge(params, %{"type" => "Create", visibility: "direct"})
290 activities = Repo.all(query)
293 |> add_link_headers(:dm_timeline, activities)
294 |> put_view(StatusView)
295 |> render("index.json", %{activities: activities, for: user, as: :activity})
298 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
299 with %Activity{} = activity <- Repo.get(Activity, id),
300 true <- ActivityPub.visible_for_user?(activity, user) do
302 |> put_view(StatusView)
303 |> try_render("status.json", %{activity: activity, for: user})
307 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
308 with %Activity{} = activity <- Repo.get(Activity, id),
310 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
311 "blocking_user" => user,
315 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
317 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
318 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
324 activities: grouped_activities[true] || [],
328 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
333 activities: grouped_activities[false] || [],
337 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
344 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
345 when length(media_ids) > 0 do
348 |> Map.put("status", ".")
350 post_status(conn, params)
353 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
356 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
359 case get_req_header(conn, "idempotency-key") do
361 _ -> Ecto.UUID.generate()
365 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
368 |> put_view(StatusView)
369 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
372 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
373 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
379 |> json(%{error: "Can't delete this post"})
383 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
384 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
386 |> put_view(StatusView)
387 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
391 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
392 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
393 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
395 |> put_view(StatusView)
396 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
400 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
401 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
402 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
404 |> put_view(StatusView)
405 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
409 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
411 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
413 |> put_view(StatusView)
414 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
418 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
419 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
421 |> put_view(StatusView)
422 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
426 |> put_resp_content_type("application/json")
427 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
431 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
432 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
434 |> put_view(StatusView)
435 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
439 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
440 with %Activity{} = activity <- Repo.get(Activity, id),
441 %User{} = user <- User.get_by_nickname(user.nickname),
442 true <- ActivityPub.visible_for_user?(activity, user),
443 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
445 |> put_view(StatusView)
446 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
450 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
451 with %Activity{} = activity <- Repo.get(Activity, id),
452 %User{} = user <- User.get_by_nickname(user.nickname),
453 true <- ActivityPub.visible_for_user?(activity, user),
454 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
456 |> put_view(StatusView)
457 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
461 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
462 activity = Activity.get_by_id(id)
464 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
466 |> put_view(StatusView)
467 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
471 |> put_resp_content_type("application/json")
472 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
476 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
477 activity = Activity.get_by_id(id)
479 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
481 |> put_view(StatusView)
482 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
486 def notifications(%{assigns: %{user: user}} = conn, params) do
487 notifications = Notification.for_user(user, params)
491 |> Enum.map(fn x -> render_notification(user, x) end)
495 |> add_link_headers(:notifications, notifications)
499 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
500 with {:ok, notification} <- Notification.get(user, id) do
501 json(conn, render_notification(user, notification))
505 |> put_resp_content_type("application/json")
506 |> send_resp(403, Jason.encode!(%{"error" => reason}))
510 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
511 Notification.clear(user)
515 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
516 with {:ok, _notif} <- Notification.dismiss(user, id) do
521 |> put_resp_content_type("application/json")
522 |> send_resp(403, Jason.encode!(%{"error" => reason}))
526 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
528 q = from(u in User, where: u.id in ^id)
529 targets = Repo.all(q)
532 |> put_view(AccountView)
533 |> render("relationships.json", %{user: user, targets: targets})
536 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
537 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
539 def update_media(%{assigns: %{user: user}} = conn, data) do
540 with %Object{} = object <- Repo.get(Object, data["id"]),
541 true <- Object.authorize_mutation(object, user),
542 true <- is_binary(data["description"]),
543 description <- data["description"] do
544 new_data = %{object.data | "name" => description}
548 |> Object.change(%{data: new_data})
551 attachment_data = Map.put(new_data, "id", object.id)
554 |> put_view(StatusView)
555 |> render("attachment.json", %{attachment: attachment_data})
559 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
560 with {:ok, object} <-
563 actor: User.ap_id(user),
564 description: Map.get(data, "description")
566 attachment_data = Map.put(object.data, "id", object.id)
569 |> put_view(StatusView)
570 |> render("attachment.json", %{attachment: attachment_data})
574 def favourited_by(conn, %{"id" => id}) do
575 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
576 q = from(u in User, where: u.ap_id in ^likes)
580 |> put_view(AccountView)
581 |> render(AccountView, "accounts.json", %{users: users, as: :user})
587 def reblogged_by(conn, %{"id" => id}) do
588 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
589 q = from(u in User, where: u.ap_id in ^announces)
593 |> put_view(AccountView)
594 |> render("accounts.json", %{users: users, as: :user})
600 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
601 local_only = params["local"] in [true, "True", "true", "1"]
604 [params["tag"], params["any"]]
608 |> Enum.map(&String.downcase(&1))
613 |> Enum.map(&String.downcase(&1))
618 |> Enum.map(&String.downcase(&1))
622 |> Map.put("type", "Create")
623 |> Map.put("local_only", local_only)
624 |> Map.put("blocking_user", user)
625 |> Map.put("muting_user", user)
626 |> Map.put("tag", tags)
627 |> Map.put("tag_all", tag_all)
628 |> Map.put("tag_reject", tag_reject)
629 |> ActivityPub.fetch_public_activities()
633 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
634 |> put_view(StatusView)
635 |> render("index.json", %{activities: activities, for: user, as: :activity})
638 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
639 with %User{} = user <- Repo.get(User, id),
640 {:ok, followers} <- User.get_followers(user) do
643 for_user && user.id == for_user.id -> followers
644 user.info.hide_followers -> []
649 |> put_view(AccountView)
650 |> render("accounts.json", %{users: followers, as: :user})
654 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
655 with %User{} = user <- Repo.get(User, id),
656 {:ok, followers} <- User.get_friends(user) do
659 for_user && user.id == for_user.id -> followers
660 user.info.hide_follows -> []
665 |> put_view(AccountView)
666 |> render("accounts.json", %{users: followers, as: :user})
670 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
671 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
673 |> put_view(AccountView)
674 |> render("accounts.json", %{users: follow_requests, as: :user})
678 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
679 with %User{} = follower <- Repo.get(User, id),
680 {:ok, follower} <- User.maybe_follow(follower, followed),
681 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
682 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
684 ActivityPub.accept(%{
685 to: [follower.ap_id],
687 object: follow_activity.data["id"],
691 |> put_view(AccountView)
692 |> render("relationship.json", %{user: followed, target: follower})
696 |> put_resp_content_type("application/json")
697 |> send_resp(403, Jason.encode!(%{"error" => message}))
701 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
702 with %User{} = follower <- Repo.get(User, id),
703 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
704 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
706 ActivityPub.reject(%{
707 to: [follower.ap_id],
709 object: follow_activity.data["id"],
713 |> put_view(AccountView)
714 |> render("relationship.json", %{user: followed, target: follower})
718 |> put_resp_content_type("application/json")
719 |> send_resp(403, Jason.encode!(%{"error" => message}))
723 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
724 with %User{} = followed <- Repo.get(User, id),
725 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
726 {:ok, _activity} <- ActivityPub.follow(follower, followed),
727 {:ok, follower, followed} <-
728 User.wait_and_refresh(
729 Config.get([:activitypub, :follow_handshake_timeout]),
734 |> put_view(AccountView)
735 |> render("relationship.json", %{user: follower, target: followed})
739 |> put_resp_content_type("application/json")
740 |> send_resp(403, Jason.encode!(%{"error" => message}))
744 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
745 with %User{} = followed <- Repo.get_by(User, nickname: uri),
746 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
747 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
749 |> put_view(AccountView)
750 |> render("account.json", %{user: followed, for: follower})
754 |> put_resp_content_type("application/json")
755 |> send_resp(403, Jason.encode!(%{"error" => message}))
759 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
760 with %User{} = followed <- Repo.get(User, id),
761 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
762 {:ok, follower, _} <- User.unfollow(follower, followed) do
764 |> put_view(AccountView)
765 |> render("relationship.json", %{user: follower, target: followed})
769 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
770 with %User{} = muted <- Repo.get(User, id),
771 {:ok, muter} <- User.mute(muter, muted) do
772 render(conn, AccountView, "relationship.json", %{user: muter, target: muted})
776 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
777 with %User{} = muted <- Repo.get(User, id),
778 {:ok, muter} <- User.unmute(muter, muted) do
779 render(conn, AccountView, "relationship.json", %{user: muter, target: muted})
783 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
784 with %User{} = blocked <- Repo.get(User, id),
785 {:ok, blocker} <- User.block(blocker, blocked),
786 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
788 |> put_view(AccountView)
789 |> render("relationship.json", %{user: blocker, target: blocked})
793 |> put_resp_content_type("application/json")
794 |> send_resp(403, Jason.encode!(%{"error" => message}))
798 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
799 with %User{} = blocked <- Repo.get(User, id),
800 {:ok, blocker} <- User.unblock(blocker, blocked),
801 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
803 |> put_view(AccountView)
804 |> render("relationship.json", %{user: blocker, target: blocked})
808 |> put_resp_content_type("application/json")
809 |> send_resp(403, Jason.encode!(%{"error" => message}))
813 def blocks(%{assigns: %{user: user}} = conn, _) do
814 with blocked_accounts <- User.blocked_users(user) do
815 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
820 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
821 json(conn, info.domain_blocks || [])
824 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
825 User.block_domain(blocker, domain)
829 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
830 User.unblock_domain(blocker, domain)
834 def status_search(user, query) do
836 if Regex.match?(~r/https?:/, query) do
837 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
838 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
839 true <- ActivityPub.visible_for_user?(activity, user) do
849 where: fragment("?->>'type' = 'Create'", a.data),
850 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
853 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
858 order_by: [desc: :id]
861 Repo.all(q) ++ fetched
864 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
865 accounts = User.search(query, params["resolve"] == "true", user)
867 statuses = status_search(user, query)
869 tags_path = Web.base_url() <> "/tag/"
875 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
876 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
877 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
880 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
882 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
889 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
890 accounts = User.search(query, params["resolve"] == "true", user)
892 statuses = status_search(user, query)
898 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
899 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
902 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
904 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
911 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
912 accounts = User.search(query, params["resolve"] == "true", user)
914 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
919 def favourites(%{assigns: %{user: user}} = conn, params) do
922 |> Map.put("type", "Create")
923 |> Map.put("favorited_by", user.ap_id)
924 |> Map.put("blocking_user", user)
925 |> ActivityPub.fetch_public_activities()
929 |> add_link_headers(:favourites, activities)
930 |> put_view(StatusView)
931 |> render("index.json", %{activities: activities, for: user, as: :activity})
934 def bookmarks(%{assigns: %{user: user}} = conn, _) do
935 user = Repo.get(User, user.id)
939 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
943 |> put_view(StatusView)
944 |> render("index.json", %{activities: activities, for: user, as: :activity})
947 def get_lists(%{assigns: %{user: user}} = conn, opts) do
948 lists = Pleroma.List.for_user(user, opts)
949 res = ListView.render("lists.json", lists: lists)
953 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
954 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
955 res = ListView.render("list.json", list: list)
961 |> json(%{error: "Record not found"})
965 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
966 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
967 res = ListView.render("lists.json", lists: lists)
971 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
972 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
973 {:ok, _list} <- Pleroma.List.delete(list) do
981 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
982 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
983 res = ListView.render("list.json", list: list)
988 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
990 |> Enum.each(fn account_id ->
991 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
992 %User{} = followed <- Repo.get(User, account_id) do
993 Pleroma.List.follow(list, followed)
1000 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1002 |> Enum.each(fn account_id ->
1003 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1004 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1005 Pleroma.List.unfollow(list, followed)
1012 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1013 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1014 {:ok, users} = Pleroma.List.get_following(list) do
1016 |> put_view(AccountView)
1017 |> render("accounts.json", %{users: users, as: :user})
1021 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1022 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1023 {:ok, list} <- Pleroma.List.rename(list, title) do
1024 res = ListView.render("list.json", list: list)
1032 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1033 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1036 |> Map.put("type", "Create")
1037 |> Map.put("blocking_user", user)
1038 |> Map.put("muting_user", user)
1040 # we must filter the following list for the user to avoid leaking statuses the user
1041 # does not actually have permission to see (for more info, peruse security issue #270).
1044 |> Enum.filter(fn x -> x in user.following end)
1045 |> ActivityPub.fetch_activities_bounded(following, params)
1049 |> put_view(StatusView)
1050 |> render("index.json", %{activities: activities, for: user, as: :activity})
1055 |> json(%{error: "Error."})
1059 def index(%{assigns: %{user: user}} = conn, _params) do
1062 |> get_session(:oauth_token)
1065 mastodon_emoji = mastodonized_emoji()
1067 limit = Config.get([:instance, :limit])
1070 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1072 flavour = get_user_flavour(user)
1077 streaming_api_base_url:
1078 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1079 access_token: token,
1081 domain: Pleroma.Web.Endpoint.host(),
1084 unfollow_modal: false,
1087 auto_play_gif: false,
1088 display_sensitive_media: false,
1089 reduce_motion: false,
1090 max_toot_chars: limit
1093 delete_others_notice: present?(user.info.is_moderator),
1094 admin: present?(user.info.is_admin)
1098 default_privacy: user.info.default_scope,
1099 default_sensitive: false
1101 media_attachments: %{
1102 accept_content_types: [
1118 user.info.settings ||
1148 push_subscription: nil,
1150 custom_emojis: mastodon_emoji,
1156 |> put_layout(false)
1157 |> put_view(MastodonView)
1158 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1161 |> redirect(to: "/web/login")
1165 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1166 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1168 with changeset <- Ecto.Changeset.change(user),
1169 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1170 {:ok, _user} <- User.update_and_set_cache(changeset) do
1175 |> put_resp_content_type("application/json")
1176 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1180 @supported_flavours ["glitch", "vanilla"]
1182 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1183 when flavour in @supported_flavours do
1184 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1186 with changeset <- Ecto.Changeset.change(user),
1187 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1188 {:ok, user} <- User.update_and_set_cache(changeset),
1189 flavour <- user.info.flavour do
1194 |> put_resp_content_type("application/json")
1195 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1199 def set_flavour(conn, _params) do
1202 |> json(%{error: "Unsupported flavour"})
1205 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1206 json(conn, get_user_flavour(user))
1209 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1213 defp get_user_flavour(_) do
1217 def login(conn, %{"code" => code}) do
1218 with {:ok, app} <- get_or_make_app(),
1219 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1220 {:ok, token} <- Token.exchange_token(app, auth) do
1222 |> put_session(:oauth_token, token.token)
1223 |> redirect(to: "/web/getting-started")
1227 def login(conn, _) do
1228 with {:ok, app} <- get_or_make_app() do
1233 response_type: "code",
1234 client_id: app.client_id,
1240 |> redirect(to: path)
1244 defp get_or_make_app() do
1245 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1247 with %App{} = app <- Repo.get_by(App, find_attrs) do
1251 cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
1257 def logout(conn, _) do
1260 |> redirect(to: "/")
1263 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1264 Logger.debug("Unimplemented, returning unmodified relationship")
1266 with %User{} = target <- Repo.get(User, id) do
1268 |> put_view(AccountView)
1269 |> render("relationship.json", %{user: user, target: target})
1273 def empty_array(conn, _) do
1274 Logger.debug("Unimplemented, returning an empty array")
1278 def empty_object(conn, _) do
1279 Logger.debug("Unimplemented, returning an empty object")
1283 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1284 actor = User.get_cached_by_ap_id(activity.data["actor"])
1285 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1286 mastodon_type = Activity.mastodon_notification_type(activity)
1290 type: mastodon_type,
1291 created_at: CommonAPI.Utils.to_masto_date(created_at),
1292 account: AccountView.render("account.json", %{user: actor, for: user})
1295 case mastodon_type do
1299 status: StatusView.render("status.json", %{activity: activity, for: user})
1305 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1311 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1322 def get_filters(%{assigns: %{user: user}} = conn, _) do
1323 filters = Filter.get_filters(user)
1324 res = FilterView.render("filters.json", filters: filters)
1329 %{assigns: %{user: user}} = conn,
1330 %{"phrase" => phrase, "context" => context} = params
1336 hide: Map.get(params, "irreversible", nil),
1337 whole_word: Map.get(params, "boolean", true)
1341 {:ok, response} = Filter.create(query)
1342 res = FilterView.render("filter.json", filter: response)
1346 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1347 filter = Filter.get(filter_id, user)
1348 res = FilterView.render("filter.json", filter: filter)
1353 %{assigns: %{user: user}} = conn,
1354 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1358 filter_id: filter_id,
1361 hide: Map.get(params, "irreversible", nil),
1362 whole_word: Map.get(params, "boolean", true)
1366 {:ok, response} = Filter.update(query)
1367 res = FilterView.render("filter.json", filter: response)
1371 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1374 filter_id: filter_id
1377 {:ok, _} = Filter.delete(query)
1381 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1382 true = Push.enabled()
1383 Subscription.delete_if_exists(user, token)
1384 {:ok, subscription} = Subscription.create(user, token, params)
1385 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1389 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1390 true = Push.enabled()
1391 subscription = Subscription.get(user, token)
1392 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1396 def update_push_subscription(
1397 %{assigns: %{user: user, token: token}} = conn,
1400 true = Push.enabled()
1401 {:ok, subscription} = Subscription.update(user, token, params)
1402 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1406 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1407 true = Push.enabled()
1408 {:ok, _response} = Subscription.delete(user, token)
1412 def errors(conn, _) do
1415 |> json("Something went wrong")
1418 def suggestions(%{assigns: %{user: user}} = conn, _) do
1419 suggestions = Config.get(:suggestions)
1421 if Keyword.get(suggestions, :enabled, false) do
1422 api = Keyword.get(suggestions, :third_party_engine, "")
1423 timeout = Keyword.get(suggestions, :timeout, 5000)
1424 limit = Keyword.get(suggestions, :limit, 23)
1426 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1428 user = user.nickname
1432 |> String.replace("{{host}}", host)
1433 |> String.replace("{{user}}", user)
1435 with {:ok, %{status: 200, body: body}} <-
1441 recv_timeout: timeout,
1445 {:ok, data} <- Jason.decode(body) do
1448 |> Enum.slice(0, limit)
1453 case User.get_or_fetch(x["acct"]) do
1460 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1463 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1469 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1476 def status_card(conn, %{"id" => status_id}) do
1477 with %Activity{} = activity <- Repo.get(Activity, status_id),
1478 true <- ActivityPub.is_public?(activity) do
1482 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1492 def try_render(conn, target, params)
1493 when is_binary(target) do
1494 res = render(conn, target, params)
1499 |> json(%{error: "Can't display this activity"})
1505 def try_render(conn, _, _) do
1508 |> json(%{error: "Can't display this activity"})
1511 defp present?(nil), do: false
1512 defp present?(false), do: false
1513 defp present?(_), do: true