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
11 alias Pleroma.Notification
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.FilterView
22 alias Pleroma.Web.MastodonAPI.ListView
23 alias Pleroma.Web.MastodonAPI.MastodonAPI
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.NotificationView
26 alias Pleroma.Web.MastodonAPI.ReportView
27 alias Pleroma.Web.MastodonAPI.StatusView
28 alias Pleroma.Web.MediaProxy
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" => nickname_or_id}) do
136 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_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
196 |> Map.drop(["since_id", "max_id"])
199 last = List.last(activities)
200 first = List.first(activities)
206 {next_url, prev_url} =
210 Pleroma.Web.Endpoint,
213 Map.merge(params, %{max_id: min})
216 Pleroma.Web.Endpoint,
219 Map.merge(params, %{since_id: max})
225 Pleroma.Web.Endpoint,
227 Map.merge(params, %{max_id: min})
230 Pleroma.Web.Endpoint,
232 Map.merge(params, %{since_id: max})
238 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
244 def home_timeline(%{assigns: %{user: user}} = conn, params) do
247 |> Map.put("type", ["Create", "Announce"])
248 |> Map.put("blocking_user", user)
249 |> Map.put("muting_user", user)
250 |> Map.put("user", user)
253 [user.ap_id | user.following]
254 |> ActivityPub.fetch_activities(params)
255 |> ActivityPub.contain_timeline(user)
259 |> add_link_headers(:home_timeline, activities)
260 |> put_view(StatusView)
261 |> render("index.json", %{activities: activities, for: user, as: :activity})
264 def public_timeline(%{assigns: %{user: user}} = conn, params) do
265 local_only = params["local"] in [true, "True", "true", "1"]
269 |> Map.put("type", ["Create", "Announce"])
270 |> Map.put("local_only", local_only)
271 |> Map.put("blocking_user", user)
272 |> Map.put("muting_user", user)
273 |> ActivityPub.fetch_public_activities()
277 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
278 |> put_view(StatusView)
279 |> render("index.json", %{activities: activities, for: user, as: :activity})
282 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
283 with %User{} = user <- Repo.get(User, params["id"]) do
284 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
287 |> add_link_headers(:user_statuses, activities, params["id"])
288 |> put_view(StatusView)
289 |> render("index.json", %{
290 activities: activities,
297 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
300 |> Map.put("type", "Create")
301 |> Map.put("blocking_user", user)
302 |> Map.put("user", user)
303 |> Map.put(:visibility, "direct")
307 |> ActivityPub.fetch_activities_query(params)
311 |> add_link_headers(:dm_timeline, activities)
312 |> put_view(StatusView)
313 |> render("index.json", %{activities: activities, for: user, as: :activity})
316 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
317 with %Activity{} = activity <- Repo.get(Activity, id),
318 true <- Visibility.visible_for_user?(activity, user) do
320 |> put_view(StatusView)
321 |> try_render("status.json", %{activity: activity, for: user})
325 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
326 with %Activity{} = activity <- Repo.get(Activity, id),
328 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
329 "blocking_user" => user,
333 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
335 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
336 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
342 activities: grouped_activities[true] || [],
346 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
351 activities: grouped_activities[false] || [],
355 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
362 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
363 when length(media_ids) > 0 do
366 |> Map.put("status", ".")
368 post_status(conn, params)
371 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
374 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
377 case get_req_header(conn, "idempotency-key") do
379 _ -> Ecto.UUID.generate()
383 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
386 |> put_view(StatusView)
387 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
390 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
391 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
397 |> json(%{error: "Can't delete this post"})
401 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
402 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
404 |> put_view(StatusView)
405 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
409 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(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 fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
419 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
420 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
422 |> put_view(StatusView)
423 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
427 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
428 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
429 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
431 |> put_view(StatusView)
432 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
436 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
437 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
439 |> put_view(StatusView)
440 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
444 |> put_resp_content_type("application/json")
445 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
449 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
450 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
452 |> put_view(StatusView)
453 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
457 def bookmark_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 <- Visibility.visible_for_user?(activity, user),
461 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
463 |> put_view(StatusView)
464 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
468 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
469 with %Activity{} = activity <- Repo.get(Activity, id),
470 %User{} = user <- User.get_by_nickname(user.nickname),
471 true <- Visibility.visible_for_user?(activity, user),
472 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
474 |> put_view(StatusView)
475 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
479 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 activity = Activity.get_by_id(id)
482 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
484 |> put_view(StatusView)
485 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
489 |> put_resp_content_type("application/json")
490 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
494 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
495 activity = Activity.get_by_id(id)
497 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
499 |> put_view(StatusView)
500 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
504 def notifications(%{assigns: %{user: user}} = conn, params) do
505 notifications = MastodonAPI.get_notifications(user, params)
508 |> add_link_headers(:notifications, notifications)
509 |> put_view(NotificationView)
510 |> render("index.json", %{notifications: notifications, for: user})
513 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
514 with {:ok, notification} <- Notification.get(user, id) do
516 |> put_view(NotificationView)
517 |> render("show.json", %{notification: notification, for: user})
521 |> put_resp_content_type("application/json")
522 |> send_resp(403, Jason.encode!(%{"error" => reason}))
526 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
527 Notification.clear(user)
531 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
532 with {:ok, _notif} <- Notification.dismiss(user, id) do
537 |> put_resp_content_type("application/json")
538 |> send_resp(403, Jason.encode!(%{"error" => reason}))
542 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
544 q = from(u in User, where: u.id in ^id)
545 targets = Repo.all(q)
548 |> put_view(AccountView)
549 |> render("relationships.json", %{user: user, targets: targets})
552 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
553 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
555 def update_media(%{assigns: %{user: user}} = conn, data) do
556 with %Object{} = object <- Repo.get(Object, data["id"]),
557 true <- Object.authorize_mutation(object, user),
558 true <- is_binary(data["description"]),
559 description <- data["description"] do
560 new_data = %{object.data | "name" => description}
564 |> Object.change(%{data: new_data})
567 attachment_data = Map.put(new_data, "id", object.id)
570 |> put_view(StatusView)
571 |> render("attachment.json", %{attachment: attachment_data})
575 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
576 with {:ok, object} <-
579 actor: User.ap_id(user),
580 description: Map.get(data, "description")
582 attachment_data = Map.put(object.data, "id", object.id)
585 |> put_view(StatusView)
586 |> render("attachment.json", %{attachment: attachment_data})
590 def favourited_by(conn, %{"id" => id}) do
591 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
592 q = from(u in User, where: u.ap_id in ^likes)
596 |> put_view(AccountView)
597 |> render(AccountView, "accounts.json", %{users: users, as: :user})
603 def reblogged_by(conn, %{"id" => id}) do
604 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
605 q = from(u in User, where: u.ap_id in ^announces)
609 |> put_view(AccountView)
610 |> render("accounts.json", %{users: users, as: :user})
616 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
617 local_only = params["local"] in [true, "True", "true", "1"]
620 [params["tag"], params["any"]]
624 |> Enum.map(&String.downcase(&1))
629 |> Enum.map(&String.downcase(&1))
634 |> Enum.map(&String.downcase(&1))
638 |> Map.put("type", "Create")
639 |> Map.put("local_only", local_only)
640 |> Map.put("blocking_user", user)
641 |> Map.put("muting_user", user)
642 |> Map.put("tag", tags)
643 |> Map.put("tag_all", tag_all)
644 |> Map.put("tag_reject", tag_reject)
645 |> ActivityPub.fetch_public_activities()
649 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
650 |> put_view(StatusView)
651 |> render("index.json", %{activities: activities, for: user, as: :activity})
654 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
655 with %User{} = user <- Repo.get(User, id),
656 followers <- MastodonAPI.get_followers(user, params) do
659 for_user && user.id == for_user.id -> followers
660 user.info.hide_followers -> []
665 |> add_link_headers(:followers, followers, user)
666 |> put_view(AccountView)
667 |> render("accounts.json", %{users: followers, as: :user})
671 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
672 with %User{} = user <- Repo.get(User, id),
673 followers <- MastodonAPI.get_friends(user, params) do
676 for_user && user.id == for_user.id -> followers
677 user.info.hide_follows -> []
682 |> add_link_headers(:following, followers, user)
683 |> put_view(AccountView)
684 |> render("accounts.json", %{users: followers, as: :user})
688 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
689 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
691 |> put_view(AccountView)
692 |> render("accounts.json", %{users: follow_requests, as: :user})
696 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
697 with %User{} = follower <- Repo.get(User, id),
698 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
700 |> put_view(AccountView)
701 |> render("relationship.json", %{user: followed, target: follower})
705 |> put_resp_content_type("application/json")
706 |> send_resp(403, Jason.encode!(%{"error" => message}))
710 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
711 with %User{} = follower <- Repo.get(User, id),
712 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
714 |> put_view(AccountView)
715 |> render("relationship.json", %{user: followed, target: follower})
719 |> put_resp_content_type("application/json")
720 |> send_resp(403, Jason.encode!(%{"error" => message}))
724 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
725 with %User{} = followed <- Repo.get(User, id),
726 false <- User.following?(follower, followed),
727 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
729 |> put_view(AccountView)
730 |> render("relationship.json", %{user: follower, target: followed})
733 followed = User.get_cached_by_id(id)
736 case conn.params["reblogs"] do
737 true -> CommonAPI.show_reblogs(follower, followed)
738 false -> CommonAPI.hide_reblogs(follower, followed)
742 |> put_view(AccountView)
743 |> render("relationship.json", %{user: follower, target: followed})
747 |> put_resp_content_type("application/json")
748 |> send_resp(403, Jason.encode!(%{"error" => message}))
752 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
753 with %User{} = followed <- Repo.get_by(User, nickname: uri),
754 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
756 |> put_view(AccountView)
757 |> render("account.json", %{user: followed, for: follower})
761 |> put_resp_content_type("application/json")
762 |> send_resp(403, Jason.encode!(%{"error" => message}))
766 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
767 with %User{} = followed <- Repo.get(User, id),
768 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
770 |> put_view(AccountView)
771 |> render("relationship.json", %{user: follower, target: followed})
775 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
776 with %User{} = muted <- Repo.get(User, id),
777 {:ok, muter} <- User.mute(muter, muted) do
779 |> put_view(AccountView)
780 |> render("relationship.json", %{user: muter, target: muted})
784 |> put_resp_content_type("application/json")
785 |> send_resp(403, Jason.encode!(%{"error" => message}))
789 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
790 with %User{} = muted <- Repo.get(User, id),
791 {:ok, muter} <- User.unmute(muter, muted) do
793 |> put_view(AccountView)
794 |> render("relationship.json", %{user: muter, target: muted})
798 |> put_resp_content_type("application/json")
799 |> send_resp(403, Jason.encode!(%{"error" => message}))
803 def mutes(%{assigns: %{user: user}} = conn, _) do
804 with muted_accounts <- User.muted_users(user) do
805 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
810 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
811 with %User{} = blocked <- Repo.get(User, id),
812 {:ok, blocker} <- User.block(blocker, blocked),
813 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
815 |> put_view(AccountView)
816 |> render("relationship.json", %{user: blocker, target: blocked})
820 |> put_resp_content_type("application/json")
821 |> send_resp(403, Jason.encode!(%{"error" => message}))
825 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
826 with %User{} = blocked <- Repo.get(User, id),
827 {:ok, blocker} <- User.unblock(blocker, blocked),
828 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
830 |> put_view(AccountView)
831 |> render("relationship.json", %{user: blocker, target: blocked})
835 |> put_resp_content_type("application/json")
836 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 def blocks(%{assigns: %{user: user}} = conn, _) do
841 with blocked_accounts <- User.blocked_users(user) do
842 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
847 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
848 json(conn, info.domain_blocks || [])
851 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
852 User.block_domain(blocker, domain)
856 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
857 User.unblock_domain(blocker, domain)
861 def status_search(user, query) do
863 if Regex.match?(~r/https?:/, query) do
864 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
865 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
866 true <- Visibility.visible_for_user?(activity, user) do
876 where: fragment("?->>'type' = 'Create'", a.data),
877 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
880 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
885 order_by: [desc: :id]
888 Repo.all(q) ++ fetched
891 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
892 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
894 statuses = status_search(user, query)
896 tags_path = Web.base_url() <> "/tag/"
902 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
903 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
904 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
907 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
909 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
916 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
917 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
919 statuses = status_search(user, query)
925 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
926 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
929 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
931 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
938 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
939 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
941 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
946 def favourites(%{assigns: %{user: user}} = conn, params) do
949 |> Map.put("type", "Create")
950 |> Map.put("favorited_by", user.ap_id)
951 |> Map.put("blocking_user", user)
954 ActivityPub.fetch_activities([], params)
958 |> add_link_headers(:favourites, activities)
959 |> put_view(StatusView)
960 |> render("index.json", %{activities: activities, for: user, as: :activity})
963 def bookmarks(%{assigns: %{user: user}} = conn, _) do
964 user = Repo.get(User, user.id)
968 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
972 |> put_view(StatusView)
973 |> render("index.json", %{activities: activities, for: user, as: :activity})
976 def get_lists(%{assigns: %{user: user}} = conn, opts) do
977 lists = Pleroma.List.for_user(user, opts)
978 res = ListView.render("lists.json", lists: lists)
982 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
983 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
984 res = ListView.render("list.json", list: list)
990 |> json(%{error: "Record not found"})
994 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
995 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
996 res = ListView.render("lists.json", lists: lists)
1000 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1001 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1002 {:ok, _list} <- Pleroma.List.delete(list) do
1010 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1011 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1012 res = ListView.render("list.json", list: list)
1017 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1019 |> Enum.each(fn account_id ->
1020 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1021 %User{} = followed <- Repo.get(User, account_id) do
1022 Pleroma.List.follow(list, followed)
1029 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1031 |> Enum.each(fn account_id ->
1032 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1033 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1034 Pleroma.List.unfollow(list, followed)
1041 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1042 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1043 {:ok, users} = Pleroma.List.get_following(list) do
1045 |> put_view(AccountView)
1046 |> render("accounts.json", %{users: users, as: :user})
1050 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1051 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1052 {:ok, list} <- Pleroma.List.rename(list, title) do
1053 res = ListView.render("list.json", list: list)
1061 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1062 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1065 |> Map.put("type", "Create")
1066 |> Map.put("blocking_user", user)
1067 |> Map.put("muting_user", user)
1069 # we must filter the following list for the user to avoid leaking statuses the user
1070 # does not actually have permission to see (for more info, peruse security issue #270).
1073 |> Enum.filter(fn x -> x in user.following end)
1074 |> ActivityPub.fetch_activities_bounded(following, params)
1078 |> put_view(StatusView)
1079 |> render("index.json", %{activities: activities, for: user, as: :activity})
1084 |> json(%{error: "Error."})
1088 def index(%{assigns: %{user: user}} = conn, _params) do
1091 |> get_session(:oauth_token)
1094 mastodon_emoji = mastodonized_emoji()
1096 limit = Config.get([:instance, :limit])
1099 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1101 flavour = get_user_flavour(user)
1106 streaming_api_base_url:
1107 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1108 access_token: token,
1110 domain: Pleroma.Web.Endpoint.host(),
1113 unfollow_modal: false,
1116 auto_play_gif: false,
1117 display_sensitive_media: false,
1118 reduce_motion: false,
1119 max_toot_chars: limit
1122 delete_others_notice: present?(user.info.is_moderator),
1123 admin: present?(user.info.is_admin)
1127 default_privacy: user.info.default_scope,
1128 default_sensitive: false,
1129 allow_content_types: Config.get([:instance, :allowed_post_formats])
1131 media_attachments: %{
1132 accept_content_types: [
1148 user.info.settings ||
1178 push_subscription: nil,
1180 custom_emojis: mastodon_emoji,
1186 |> put_layout(false)
1187 |> put_view(MastodonView)
1188 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1191 |> redirect(to: "/web/login")
1195 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1196 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1198 with changeset <- Ecto.Changeset.change(user),
1199 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1200 {:ok, _user} <- User.update_and_set_cache(changeset) do
1205 |> put_resp_content_type("application/json")
1206 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1210 @supported_flavours ["glitch", "vanilla"]
1212 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1213 when flavour in @supported_flavours do
1214 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1216 with changeset <- Ecto.Changeset.change(user),
1217 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1218 {:ok, user} <- User.update_and_set_cache(changeset),
1219 flavour <- user.info.flavour do
1224 |> put_resp_content_type("application/json")
1225 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1229 def set_flavour(conn, _params) do
1232 |> json(%{error: "Unsupported flavour"})
1235 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1236 json(conn, get_user_flavour(user))
1239 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1243 defp get_user_flavour(_) do
1247 def login(conn, %{"code" => code}) do
1248 with {:ok, app} <- get_or_make_app(),
1249 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1250 {:ok, token} <- Token.exchange_token(app, auth) do
1252 |> put_session(:oauth_token, token.token)
1253 |> redirect(to: "/web/getting-started")
1257 def login(conn, _) do
1258 with {:ok, app} <- get_or_make_app() do
1263 response_type: "code",
1264 client_id: app.client_id,
1266 scope: Enum.join(app.scopes, " ")
1270 |> redirect(to: path)
1274 defp get_or_make_app do
1275 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1276 scopes = ["read", "write", "follow", "push"]
1278 with %App{} = app <- Repo.get_by(App, find_attrs) do
1280 if app.scopes == scopes do
1284 |> Ecto.Changeset.change(%{scopes: scopes})
1292 App.register_changeset(
1294 Map.put(find_attrs, :scopes, scopes)
1301 def logout(conn, _) do
1304 |> redirect(to: "/")
1307 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1308 Logger.debug("Unimplemented, returning unmodified relationship")
1310 with %User{} = target <- Repo.get(User, id) do
1312 |> put_view(AccountView)
1313 |> render("relationship.json", %{user: user, target: target})
1317 def empty_array(conn, _) do
1318 Logger.debug("Unimplemented, returning an empty array")
1322 def empty_object(conn, _) do
1323 Logger.debug("Unimplemented, returning an empty object")
1327 def get_filters(%{assigns: %{user: user}} = conn, _) do
1328 filters = Filter.get_filters(user)
1329 res = FilterView.render("filters.json", filters: filters)
1334 %{assigns: %{user: user}} = conn,
1335 %{"phrase" => phrase, "context" => context} = params
1341 hide: Map.get(params, "irreversible", nil),
1342 whole_word: Map.get(params, "boolean", true)
1346 {:ok, response} = Filter.create(query)
1347 res = FilterView.render("filter.json", filter: response)
1351 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1352 filter = Filter.get(filter_id, user)
1353 res = FilterView.render("filter.json", filter: filter)
1358 %{assigns: %{user: user}} = conn,
1359 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1363 filter_id: filter_id,
1366 hide: Map.get(params, "irreversible", nil),
1367 whole_word: Map.get(params, "boolean", true)
1371 {:ok, response} = Filter.update(query)
1372 res = FilterView.render("filter.json", filter: response)
1376 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1379 filter_id: filter_id
1382 {:ok, _} = Filter.delete(query)
1388 def errors(conn, _) do
1391 |> json("Something went wrong")
1394 def suggestions(%{assigns: %{user: user}} = conn, _) do
1395 suggestions = Config.get(:suggestions)
1397 if Keyword.get(suggestions, :enabled, false) do
1398 api = Keyword.get(suggestions, :third_party_engine, "")
1399 timeout = Keyword.get(suggestions, :timeout, 5000)
1400 limit = Keyword.get(suggestions, :limit, 23)
1402 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1404 user = user.nickname
1408 |> String.replace("{{host}}", host)
1409 |> String.replace("{{user}}", user)
1411 with {:ok, %{status: 200, body: body}} <-
1416 recv_timeout: timeout,
1420 {:ok, data} <- Jason.decode(body) do
1423 |> Enum.slice(0, limit)
1428 case User.get_or_fetch(x["acct"]) do
1435 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1438 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1444 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1451 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1452 with %Activity{} = activity <- Repo.get(Activity, status_id),
1453 true <- Visibility.visible_for_user?(activity, user) do
1457 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1467 def reports(%{assigns: %{user: user}} = conn, params) do
1468 case CommonAPI.report(user, params) do
1471 |> put_view(ReportView)
1472 |> try_render("report.json", %{activity: activity})
1476 |> put_status(:bad_request)
1477 |> json(%{error: err})
1481 def try_render(conn, target, params)
1482 when is_binary(target) do
1483 res = render(conn, target, params)
1488 |> json(%{error: "Can't display this activity"})
1494 def try_render(conn, _, _) do
1497 |> json(%{error: "Can't display this activity"})
1500 defp present?(nil), do: false
1501 defp present?(false), do: false
1502 defp present?(_), do: true