1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
10 alias Pleroma.Notification
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MediaProxy
18 alias Pleroma.Web.Push
19 alias Push.Subscription
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.PushSubscriptionView
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.MastodonAPI.ReportView
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Utils
30 alias Pleroma.Web.ActivityPub.Visibility
31 alias Pleroma.Web.OAuth.App
32 alias Pleroma.Web.OAuth.Authorization
33 alias Pleroma.Web.OAuth.Token
35 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
40 @httpoison Application.get_env(:pleroma, :httpoison)
41 @local_mastodon_name "Mastodon-Local"
43 action_fallback(:errors)
45 def create_app(conn, params) do
46 scopes = oauth_scopes(params, ["read"])
50 |> Map.drop(["scope", "scopes"])
51 |> Map.put("scopes", scopes)
53 with cs <- App.register_changeset(%App{}, app_attrs),
54 false <- cs.changes[:client_name] == @local_mastodon_name,
55 {:ok, app} <- Repo.insert(cs) do
57 id: app.id |> to_string,
58 name: app.client_name,
59 client_id: app.client_id,
60 client_secret: app.client_secret,
61 redirect_uri: app.redirect_uris,
74 value_function \\ fn x -> {:ok, x} end
76 if Map.has_key?(params, params_field) do
77 case value_function.(params[params_field]) do
78 {:ok, new_value} -> Map.put(map, map_field, new_value)
86 def update_credentials(%{assigns: %{user: user}} = conn, params) do
91 |> add_if_present(params, "display_name", :name)
92 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
93 |> add_if_present(params, "avatar", :avatar, fn value ->
94 with %Plug.Upload{} <- value,
95 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
104 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
105 |> add_if_present(params, "header", :banner, fn value ->
106 with %Plug.Upload{} <- value,
107 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
114 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
116 with changeset <- User.update_changeset(user, user_params),
117 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
118 {:ok, user} <- User.update_and_set_cache(changeset) do
119 if original_user != user do
120 CommonAPI.update(user)
123 json(conn, AccountView.render("account.json", %{user: user, for: user}))
128 |> json(%{error: "Invalid request"})
132 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
133 account = AccountView.render("account.json", %{user: user, for: user})
137 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
138 with %User{} = user <- Repo.get(User, id),
139 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
140 account = AccountView.render("account.json", %{user: user, for: for_user})
146 |> json(%{error: "Can't find user"})
150 @mastodon_api_level "2.5.0"
152 def masto_instance(conn, _params) do
153 instance = Config.get(:instance)
157 title: Keyword.get(instance, :name),
158 description: Keyword.get(instance, :description),
159 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
160 email: Keyword.get(instance, :email),
162 streaming_api: Pleroma.Web.Endpoint.websocket_url()
164 stats: Stats.get_stats(),
165 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
166 max_toot_chars: Keyword.get(instance, :limit)
172 def peers(conn, _params) do
173 json(conn, Stats.get_peers())
176 defp mastodonized_emoji do
177 Pleroma.Emoji.get_all()
178 |> Enum.map(fn {shortcode, relative_url} ->
179 url = to_string(URI.merge(Web.base_url(), relative_url))
182 "shortcode" => shortcode,
184 "visible_in_picker" => true,
190 def custom_emojis(conn, _params) do
191 mastodon_emoji = mastodonized_emoji()
192 json(conn, mastodon_emoji)
195 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
196 last = List.last(activities)
197 first = List.first(activities)
203 {next_url, prev_url} =
207 Pleroma.Web.Endpoint,
210 Map.merge(params, %{max_id: min})
213 Pleroma.Web.Endpoint,
216 Map.merge(params, %{since_id: max})
222 Pleroma.Web.Endpoint,
224 Map.merge(params, %{max_id: min})
227 Pleroma.Web.Endpoint,
229 Map.merge(params, %{since_id: max})
235 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
241 def home_timeline(%{assigns: %{user: user}} = conn, params) do
244 |> Map.put("type", ["Create", "Announce"])
245 |> Map.put("blocking_user", user)
246 |> Map.put("muting_user", user)
247 |> Map.put("user", user)
250 [user.ap_id | user.following]
251 |> ActivityPub.fetch_activities(params)
252 |> ActivityPub.contain_timeline(user)
256 |> add_link_headers(:home_timeline, activities)
257 |> put_view(StatusView)
258 |> render("index.json", %{activities: activities, for: user, as: :activity})
261 def public_timeline(%{assigns: %{user: user}} = conn, params) do
262 local_only = params["local"] in [true, "True", "true", "1"]
266 |> Map.put("type", ["Create", "Announce"])
267 |> Map.put("local_only", local_only)
268 |> Map.put("blocking_user", user)
269 |> Map.put("muting_user", user)
270 |> ActivityPub.fetch_public_activities()
274 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
275 |> put_view(StatusView)
276 |> render("index.json", %{activities: activities, for: user, as: :activity})
279 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
280 with %User{} = user <- Repo.get(User, params["id"]) do
281 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
284 |> add_link_headers(:user_statuses, activities, params["id"])
285 |> put_view(StatusView)
286 |> render("index.json", %{
287 activities: activities,
294 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
296 ActivityPub.fetch_activities_query(
298 Map.merge(params, %{"type" => "Create", visibility: "direct"})
301 activities = Repo.all(query)
304 |> add_link_headers(:dm_timeline, activities)
305 |> put_view(StatusView)
306 |> render("index.json", %{activities: activities, for: user, as: :activity})
309 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
310 with %Activity{} = activity <- Repo.get(Activity, id),
311 true <- Visibility.visible_for_user?(activity, user) do
313 |> put_view(StatusView)
314 |> try_render("status.json", %{activity: activity, for: user})
318 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
319 with %Activity{} = activity <- Repo.get(Activity, id),
321 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
322 "blocking_user" => user,
326 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
328 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
329 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
335 activities: grouped_activities[true] || [],
339 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
344 activities: grouped_activities[false] || [],
348 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
355 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
356 when length(media_ids) > 0 do
359 |> Map.put("status", ".")
361 post_status(conn, params)
364 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
367 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
370 case get_req_header(conn, "idempotency-key") do
372 _ -> Ecto.UUID.generate()
376 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
379 |> put_view(StatusView)
380 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
383 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
384 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
390 |> json(%{error: "Can't delete this post"})
394 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
395 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
397 |> put_view(StatusView)
398 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
402 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
403 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
404 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
406 |> put_view(StatusView)
407 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
411 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
412 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
413 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
415 |> put_view(StatusView)
416 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
420 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
421 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
422 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
424 |> put_view(StatusView)
425 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
429 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
430 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
432 |> put_view(StatusView)
433 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
437 |> put_resp_content_type("application/json")
438 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
442 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
443 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
445 |> put_view(StatusView)
446 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
450 def bookmark_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 <- Visibility.visible_for_user?(activity, user),
454 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
456 |> put_view(StatusView)
457 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
461 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
462 with %Activity{} = activity <- Repo.get(Activity, id),
463 %User{} = user <- User.get_by_nickname(user.nickname),
464 true <- Visibility.visible_for_user?(activity, user),
465 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
467 |> put_view(StatusView)
468 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
472 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
473 activity = Activity.get_by_id(id)
475 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
477 |> put_view(StatusView)
478 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
482 |> put_resp_content_type("application/json")
483 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
487 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
488 activity = Activity.get_by_id(id)
490 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
492 |> put_view(StatusView)
493 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
497 def notifications(%{assigns: %{user: user}} = conn, params) do
498 notifications = Notification.for_user(user, params)
502 |> Enum.map(fn x -> render_notification(user, x) end)
506 |> add_link_headers(:notifications, notifications)
510 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
511 with {:ok, notification} <- Notification.get(user, id) do
512 json(conn, render_notification(user, notification))
516 |> put_resp_content_type("application/json")
517 |> send_resp(403, Jason.encode!(%{"error" => reason}))
521 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
522 Notification.clear(user)
526 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
527 with {:ok, _notif} <- Notification.dismiss(user, id) do
532 |> put_resp_content_type("application/json")
533 |> send_resp(403, Jason.encode!(%{"error" => reason}))
537 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
539 q = from(u in User, where: u.id in ^id)
540 targets = Repo.all(q)
543 |> put_view(AccountView)
544 |> render("relationships.json", %{user: user, targets: targets})
547 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
548 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
550 def update_media(%{assigns: %{user: user}} = conn, data) do
551 with %Object{} = object <- Repo.get(Object, data["id"]),
552 true <- Object.authorize_mutation(object, user),
553 true <- is_binary(data["description"]),
554 description <- data["description"] do
555 new_data = %{object.data | "name" => description}
559 |> Object.change(%{data: new_data})
562 attachment_data = Map.put(new_data, "id", object.id)
565 |> put_view(StatusView)
566 |> render("attachment.json", %{attachment: attachment_data})
570 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
571 with {:ok, object} <-
574 actor: User.ap_id(user),
575 description: Map.get(data, "description")
577 attachment_data = Map.put(object.data, "id", object.id)
580 |> put_view(StatusView)
581 |> render("attachment.json", %{attachment: attachment_data})
585 def favourited_by(conn, %{"id" => id}) do
586 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
587 q = from(u in User, where: u.ap_id in ^likes)
591 |> put_view(AccountView)
592 |> render(AccountView, "accounts.json", %{users: users, as: :user})
598 def reblogged_by(conn, %{"id" => id}) do
599 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
600 q = from(u in User, where: u.ap_id in ^announces)
604 |> put_view(AccountView)
605 |> render("accounts.json", %{users: users, as: :user})
611 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
612 local_only = params["local"] in [true, "True", "true", "1"]
615 [params["tag"], params["any"]]
619 |> Enum.map(&String.downcase(&1))
624 |> Enum.map(&String.downcase(&1))
629 |> Enum.map(&String.downcase(&1))
633 |> Map.put("type", "Create")
634 |> Map.put("local_only", local_only)
635 |> Map.put("blocking_user", user)
636 |> Map.put("muting_user", user)
637 |> Map.put("tag", tags)
638 |> Map.put("tag_all", tag_all)
639 |> Map.put("tag_reject", tag_reject)
640 |> ActivityPub.fetch_public_activities()
644 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
645 |> put_view(StatusView)
646 |> render("index.json", %{activities: activities, for: user, as: :activity})
649 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
650 with %User{} = user <- Repo.get(User, id),
651 {:ok, followers} <- User.get_followers(user) do
654 for_user && user.id == for_user.id -> followers
655 user.info.hide_followers -> []
660 |> put_view(AccountView)
661 |> render("accounts.json", %{users: followers, as: :user})
665 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
666 with %User{} = user <- Repo.get(User, id),
667 {:ok, followers} <- User.get_friends(user) do
670 for_user && user.id == for_user.id -> followers
671 user.info.hide_follows -> []
676 |> put_view(AccountView)
677 |> render("accounts.json", %{users: followers, as: :user})
681 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
682 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
684 |> put_view(AccountView)
685 |> render("accounts.json", %{users: follow_requests, as: :user})
689 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
690 with %User{} = follower <- Repo.get(User, id),
691 {:ok, follower} <- User.maybe_follow(follower, followed),
692 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
693 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
695 ActivityPub.accept(%{
696 to: [follower.ap_id],
698 object: follow_activity.data["id"],
702 |> put_view(AccountView)
703 |> render("relationship.json", %{user: followed, target: follower})
707 |> put_resp_content_type("application/json")
708 |> send_resp(403, Jason.encode!(%{"error" => message}))
712 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
713 with %User{} = follower <- Repo.get(User, id),
714 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
715 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
717 ActivityPub.reject(%{
718 to: [follower.ap_id],
720 object: follow_activity.data["id"],
724 |> put_view(AccountView)
725 |> render("relationship.json", %{user: followed, target: follower})
729 |> put_resp_content_type("application/json")
730 |> send_resp(403, Jason.encode!(%{"error" => message}))
734 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
735 with %User{} = followed <- Repo.get(User, id),
736 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
737 {:ok, _activity} <- ActivityPub.follow(follower, followed),
738 {:ok, follower, followed} <-
739 User.wait_and_refresh(
740 Config.get([:activitypub, :follow_handshake_timeout]),
745 |> put_view(AccountView)
746 |> render("relationship.json", %{user: follower, target: followed})
750 |> put_resp_content_type("application/json")
751 |> send_resp(403, Jason.encode!(%{"error" => message}))
755 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
756 with %User{} = followed <- Repo.get_by(User, nickname: uri),
757 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
758 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
760 |> put_view(AccountView)
761 |> render("account.json", %{user: followed, for: follower})
765 |> put_resp_content_type("application/json")
766 |> send_resp(403, Jason.encode!(%{"error" => message}))
770 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
771 with %User{} = followed <- Repo.get(User, id),
772 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
773 {:ok, follower, _} <- User.unfollow(follower, followed) do
775 |> put_view(AccountView)
776 |> render("relationship.json", %{user: follower, target: followed})
780 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
781 with %User{} = muted <- Repo.get(User, id),
782 {:ok, muter} <- User.mute(muter, muted) do
784 |> put_view(AccountView)
785 |> render("relationship.json", %{user: muter, target: muted})
789 |> put_resp_content_type("application/json")
790 |> send_resp(403, Jason.encode!(%{"error" => message}))
794 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
795 with %User{} = muted <- Repo.get(User, id),
796 {:ok, muter} <- User.unmute(muter, muted) do
798 |> put_view(AccountView)
799 |> render("relationship.json", %{user: muter, target: muted})
803 |> put_resp_content_type("application/json")
804 |> send_resp(403, Jason.encode!(%{"error" => message}))
808 def mutes(%{assigns: %{user: user}} = conn, _) do
809 with muted_accounts <- User.muted_users(user) do
810 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
815 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
816 with %User{} = blocked <- Repo.get(User, id),
817 {:ok, blocker} <- User.block(blocker, blocked),
818 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
820 |> put_view(AccountView)
821 |> render("relationship.json", %{user: blocker, target: blocked})
825 |> put_resp_content_type("application/json")
826 |> send_resp(403, Jason.encode!(%{"error" => message}))
830 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
831 with %User{} = blocked <- Repo.get(User, id),
832 {:ok, blocker} <- User.unblock(blocker, blocked),
833 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
835 |> put_view(AccountView)
836 |> render("relationship.json", %{user: blocker, target: blocked})
840 |> put_resp_content_type("application/json")
841 |> send_resp(403, Jason.encode!(%{"error" => message}))
845 def blocks(%{assigns: %{user: user}} = conn, _) do
846 with blocked_accounts <- User.blocked_users(user) do
847 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
852 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
853 json(conn, info.domain_blocks || [])
856 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
857 User.block_domain(blocker, domain)
861 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
862 User.unblock_domain(blocker, domain)
866 def status_search(user, query) do
868 if Regex.match?(~r/https?:/, query) do
869 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
870 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
871 true <- Visibility.visible_for_user?(activity, user) do
881 where: fragment("?->>'type' = 'Create'", a.data),
882 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
885 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
890 order_by: [desc: :id]
893 Repo.all(q) ++ fetched
896 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
897 accounts = User.search(query, params["resolve"] == "true", user)
899 statuses = status_search(user, query)
901 tags_path = Web.base_url() <> "/tag/"
907 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
908 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
909 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
912 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
914 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
921 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
922 accounts = User.search(query, params["resolve"] == "true", user)
924 statuses = status_search(user, query)
930 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
931 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
934 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
936 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
943 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
944 accounts = User.search(query, params["resolve"] == "true", user)
946 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
951 def favourites(%{assigns: %{user: user}} = conn, params) do
954 |> Map.put("type", "Create")
955 |> Map.put("favorited_by", user.ap_id)
956 |> Map.put("blocking_user", user)
957 |> ActivityPub.fetch_public_activities()
961 |> add_link_headers(:favourites, activities)
962 |> put_view(StatusView)
963 |> render("index.json", %{activities: activities, for: user, as: :activity})
966 def bookmarks(%{assigns: %{user: user}} = conn, _) do
967 user = Repo.get(User, user.id)
971 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
975 |> put_view(StatusView)
976 |> render("index.json", %{activities: activities, for: user, as: :activity})
979 def get_lists(%{assigns: %{user: user}} = conn, opts) do
980 lists = Pleroma.List.for_user(user, opts)
981 res = ListView.render("lists.json", lists: lists)
985 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
987 res = ListView.render("list.json", list: list)
993 |> json(%{error: "Record not found"})
997 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
998 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
999 res = ListView.render("lists.json", lists: lists)
1003 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1004 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1005 {:ok, _list} <- Pleroma.List.delete(list) do
1013 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1014 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1015 res = ListView.render("list.json", list: list)
1020 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1022 |> Enum.each(fn account_id ->
1023 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1024 %User{} = followed <- Repo.get(User, account_id) do
1025 Pleroma.List.follow(list, followed)
1032 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1034 |> Enum.each(fn account_id ->
1035 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1036 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1037 Pleroma.List.unfollow(list, followed)
1044 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1045 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1046 {:ok, users} = Pleroma.List.get_following(list) do
1048 |> put_view(AccountView)
1049 |> render("accounts.json", %{users: users, as: :user})
1053 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1054 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1055 {:ok, list} <- Pleroma.List.rename(list, title) do
1056 res = ListView.render("list.json", list: list)
1064 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1065 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1068 |> Map.put("type", "Create")
1069 |> Map.put("blocking_user", user)
1070 |> Map.put("muting_user", user)
1072 # we must filter the following list for the user to avoid leaking statuses the user
1073 # does not actually have permission to see (for more info, peruse security issue #270).
1076 |> Enum.filter(fn x -> x in user.following end)
1077 |> ActivityPub.fetch_activities_bounded(following, params)
1081 |> put_view(StatusView)
1082 |> render("index.json", %{activities: activities, for: user, as: :activity})
1087 |> json(%{error: "Error."})
1091 def index(%{assigns: %{user: user}} = conn, _params) do
1094 |> get_session(:oauth_token)
1097 mastodon_emoji = mastodonized_emoji()
1099 limit = Config.get([:instance, :limit])
1102 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1104 flavour = get_user_flavour(user)
1109 streaming_api_base_url:
1110 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1111 access_token: token,
1113 domain: Pleroma.Web.Endpoint.host(),
1116 unfollow_modal: false,
1119 auto_play_gif: false,
1120 display_sensitive_media: false,
1121 reduce_motion: false,
1122 max_toot_chars: limit
1125 delete_others_notice: present?(user.info.is_moderator),
1126 admin: present?(user.info.is_admin)
1130 default_privacy: user.info.default_scope,
1131 default_sensitive: false
1133 media_attachments: %{
1134 accept_content_types: [
1150 user.info.settings ||
1180 push_subscription: nil,
1182 custom_emojis: mastodon_emoji,
1188 |> put_layout(false)
1189 |> put_view(MastodonView)
1190 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1193 |> redirect(to: "/web/login")
1197 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1198 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1200 with changeset <- Ecto.Changeset.change(user),
1201 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1202 {:ok, _user} <- User.update_and_set_cache(changeset) do
1207 |> put_resp_content_type("application/json")
1208 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1212 @supported_flavours ["glitch", "vanilla"]
1214 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1215 when flavour in @supported_flavours do
1216 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1218 with changeset <- Ecto.Changeset.change(user),
1219 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1220 {:ok, user} <- User.update_and_set_cache(changeset),
1221 flavour <- user.info.flavour do
1226 |> put_resp_content_type("application/json")
1227 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1231 def set_flavour(conn, _params) do
1234 |> json(%{error: "Unsupported flavour"})
1237 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1238 json(conn, get_user_flavour(user))
1241 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1245 defp get_user_flavour(_) do
1249 def login(conn, %{"code" => code}) do
1250 with {:ok, app} <- get_or_make_app(),
1251 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1252 {:ok, token} <- Token.exchange_token(app, auth) do
1254 |> put_session(:oauth_token, token.token)
1255 |> redirect(to: "/web/getting-started")
1259 def login(conn, _) do
1260 with {:ok, app} <- get_or_make_app() do
1265 response_type: "code",
1266 client_id: app.client_id,
1268 scope: Enum.join(app.scopes, " ")
1272 |> redirect(to: path)
1276 defp get_or_make_app() do
1277 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1278 scopes = ["read", "write", "follow", "push"]
1280 with %App{} = app <- Repo.get_by(App, find_attrs) do
1282 if app.scopes == scopes do
1286 |> Ecto.Changeset.change(%{scopes: scopes})
1294 App.register_changeset(
1296 Map.put(find_attrs, :scopes, scopes)
1303 def logout(conn, _) do
1306 |> redirect(to: "/")
1309 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1310 Logger.debug("Unimplemented, returning unmodified relationship")
1312 with %User{} = target <- Repo.get(User, id) do
1314 |> put_view(AccountView)
1315 |> render("relationship.json", %{user: user, target: target})
1319 def empty_array(conn, _) do
1320 Logger.debug("Unimplemented, returning an empty array")
1324 def empty_object(conn, _) do
1325 Logger.debug("Unimplemented, returning an empty object")
1329 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1330 actor = User.get_cached_by_ap_id(activity.data["actor"])
1331 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1332 mastodon_type = Activity.mastodon_notification_type(activity)
1336 type: mastodon_type,
1337 created_at: CommonAPI.Utils.to_masto_date(created_at),
1338 account: AccountView.render("account.json", %{user: actor, for: user})
1341 case mastodon_type do
1345 status: StatusView.render("status.json", %{activity: activity, for: user})
1351 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1357 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1368 def get_filters(%{assigns: %{user: user}} = conn, _) do
1369 filters = Filter.get_filters(user)
1370 res = FilterView.render("filters.json", filters: filters)
1375 %{assigns: %{user: user}} = conn,
1376 %{"phrase" => phrase, "context" => context} = params
1382 hide: Map.get(params, "irreversible", nil),
1383 whole_word: Map.get(params, "boolean", true)
1387 {:ok, response} = Filter.create(query)
1388 res = FilterView.render("filter.json", filter: response)
1392 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1393 filter = Filter.get(filter_id, user)
1394 res = FilterView.render("filter.json", filter: filter)
1399 %{assigns: %{user: user}} = conn,
1400 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1404 filter_id: filter_id,
1407 hide: Map.get(params, "irreversible", nil),
1408 whole_word: Map.get(params, "boolean", true)
1412 {:ok, response} = Filter.update(query)
1413 res = FilterView.render("filter.json", filter: response)
1417 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1420 filter_id: filter_id
1423 {:ok, _} = Filter.delete(query)
1427 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1428 true = Push.enabled()
1429 Subscription.delete_if_exists(user, token)
1430 {:ok, subscription} = Subscription.create(user, token, params)
1431 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1435 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1436 true = Push.enabled()
1437 subscription = Subscription.get(user, token)
1438 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1442 def update_push_subscription(
1443 %{assigns: %{user: user, token: token}} = conn,
1446 true = Push.enabled()
1447 {:ok, subscription} = Subscription.update(user, token, params)
1448 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1452 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1453 true = Push.enabled()
1454 {:ok, _response} = Subscription.delete(user, token)
1458 def errors(conn, _) do
1461 |> json("Something went wrong")
1464 def suggestions(%{assigns: %{user: user}} = conn, _) do
1465 suggestions = Config.get(:suggestions)
1467 if Keyword.get(suggestions, :enabled, false) do
1468 api = Keyword.get(suggestions, :third_party_engine, "")
1469 timeout = Keyword.get(suggestions, :timeout, 5000)
1470 limit = Keyword.get(suggestions, :limit, 23)
1472 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1474 user = user.nickname
1478 |> String.replace("{{host}}", host)
1479 |> String.replace("{{user}}", user)
1481 with {:ok, %{status: 200, body: body}} <-
1487 recv_timeout: timeout,
1491 {:ok, data} <- Jason.decode(body) do
1494 |> Enum.slice(0, limit)
1499 case User.get_or_fetch(x["acct"]) do
1506 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1509 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1515 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1522 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1523 with %Activity{} = activity <- Repo.get(Activity, status_id),
1524 true <- Visibility.visible_for_user?(activity, user) do
1528 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1538 def reports(%{assigns: %{user: user}} = conn, params) do
1539 case CommonAPI.report(user, params) do
1542 |> put_view(ReportView)
1543 |> try_render("report.json", %{activity: activity})
1547 |> put_status(:bad_request)
1548 |> json(%{error: err})
1552 def try_render(conn, target, params)
1553 when is_binary(target) do
1554 res = render(conn, target, params)
1559 |> json(%{error: "Can't display this activity"})
1565 def try_render(conn, _, _) do
1568 |> json(%{error: "Can't display this activity"})
1571 defp present?(nil), do: false
1572 defp present?(false), do: false
1573 defp present?(_), do: true