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.AppView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonView
26 alias Pleroma.Web.MastodonAPI.NotificationView
27 alias Pleroma.Web.MastodonAPI.ReportView
28 alias Pleroma.Web.MastodonAPI.StatusView
29 alias Pleroma.Web.MediaProxy
30 alias Pleroma.Web.OAuth.App
31 alias Pleroma.Web.OAuth.Authorization
32 alias Pleroma.Web.OAuth.Token
34 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
39 @httpoison Application.get_env(:pleroma, :httpoison)
40 @local_mastodon_name "Mastodon-Local"
42 action_fallback(:errors)
44 def create_app(conn, params) do
45 scopes = oauth_scopes(params, ["read"])
49 |> Map.drop(["scope", "scopes"])
50 |> Map.put("scopes", scopes)
52 with cs <- App.register_changeset(%App{}, app_attrs),
53 false <- cs.changes[:client_name] == @local_mastodon_name,
54 {:ok, app} <- Repo.insert(cs) do
56 id: app.id |> to_string,
57 name: app.client_name,
58 client_id: app.client_id,
59 client_secret: app.client_secret,
60 redirect_uri: app.redirect_uris,
73 value_function \\ fn x -> {:ok, x} end
75 if Map.has_key?(params, params_field) do
76 case value_function.(params[params_field]) do
77 {:ok, new_value} -> Map.put(map, map_field, new_value)
85 def update_credentials(%{assigns: %{user: user}} = conn, params) do
90 |> add_if_present(params, "display_name", :name)
91 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
92 |> add_if_present(params, "avatar", :avatar, fn value ->
93 with %Plug.Upload{} <- value,
94 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
103 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
104 |> add_if_present(params, "header", :banner, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
113 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
115 with changeset <- User.update_changeset(user, user_params),
116 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
117 {:ok, user} <- User.update_and_set_cache(changeset) do
118 if original_user != user do
119 CommonAPI.update(user)
122 json(conn, AccountView.render("account.json", %{user: user, for: user}))
127 |> json(%{error: "Invalid request"})
131 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
132 account = AccountView.render("account.json", %{user: user, for: user})
136 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
137 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
140 |> render("show.json", %{app: app})
144 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
145 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
146 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
147 account = AccountView.render("account.json", %{user: user, for: for_user})
153 |> json(%{error: "Can't find user"})
157 @mastodon_api_level "2.5.0"
159 def masto_instance(conn, _params) do
160 instance = Config.get(:instance)
164 title: Keyword.get(instance, :name),
165 description: Keyword.get(instance, :description),
166 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
167 email: Keyword.get(instance, :email),
169 streaming_api: Pleroma.Web.Endpoint.websocket_url()
171 stats: Stats.get_stats(),
172 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
174 registrations: Pleroma.Config.get([:instance, :registrations_open]),
175 # Extra (not present in Mastodon):
176 max_toot_chars: Keyword.get(instance, :limit)
182 def peers(conn, _params) do
183 json(conn, Stats.get_peers())
186 defp mastodonized_emoji do
187 Pleroma.Emoji.get_all()
188 |> Enum.map(fn {shortcode, relative_url} ->
189 url = to_string(URI.merge(Web.base_url(), relative_url))
192 "shortcode" => shortcode,
194 "visible_in_picker" => true,
200 def custom_emojis(conn, _params) do
201 mastodon_emoji = mastodonized_emoji()
202 json(conn, mastodon_emoji)
205 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
208 |> Map.drop(["since_id", "max_id"])
211 last = List.last(activities)
212 first = List.first(activities)
218 {next_url, prev_url} =
222 Pleroma.Web.Endpoint,
225 Map.merge(params, %{max_id: min})
228 Pleroma.Web.Endpoint,
231 Map.merge(params, %{since_id: max})
237 Pleroma.Web.Endpoint,
239 Map.merge(params, %{max_id: min})
242 Pleroma.Web.Endpoint,
244 Map.merge(params, %{since_id: max})
250 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
256 def home_timeline(%{assigns: %{user: user}} = conn, params) do
259 |> Map.put("type", ["Create", "Announce"])
260 |> Map.put("blocking_user", user)
261 |> Map.put("muting_user", user)
262 |> Map.put("user", user)
265 [user.ap_id | user.following]
266 |> ActivityPub.fetch_activities(params)
267 |> ActivityPub.contain_timeline(user)
271 |> add_link_headers(:home_timeline, activities)
272 |> put_view(StatusView)
273 |> render("index.json", %{activities: activities, for: user, as: :activity})
276 def public_timeline(%{assigns: %{user: user}} = conn, params) do
277 local_only = params["local"] in [true, "True", "true", "1"]
281 |> Map.put("type", ["Create", "Announce"])
282 |> Map.put("local_only", local_only)
283 |> Map.put("blocking_user", user)
284 |> Map.put("muting_user", user)
285 |> ActivityPub.fetch_public_activities()
289 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
290 |> put_view(StatusView)
291 |> render("index.json", %{activities: activities, for: user, as: :activity})
294 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
295 with %User{} = user <- Repo.get(User, params["id"]) do
296 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
299 |> add_link_headers(:user_statuses, activities, params["id"])
300 |> put_view(StatusView)
301 |> render("index.json", %{
302 activities: activities,
309 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
312 |> Map.put("type", "Create")
313 |> Map.put("blocking_user", user)
314 |> Map.put("user", user)
315 |> Map.put(:visibility, "direct")
319 |> ActivityPub.fetch_activities_query(params)
323 |> add_link_headers(:dm_timeline, activities)
324 |> put_view(StatusView)
325 |> render("index.json", %{activities: activities, for: user, as: :activity})
328 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
329 with %Activity{} = activity <- Repo.get(Activity, id),
330 true <- Visibility.visible_for_user?(activity, user) do
332 |> put_view(StatusView)
333 |> try_render("status.json", %{activity: activity, for: user})
337 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
338 with %Activity{} = activity <- Repo.get(Activity, id),
340 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
341 "blocking_user" => user,
345 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
347 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
348 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
354 activities: grouped_activities[true] || [],
358 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
363 activities: grouped_activities[false] || [],
367 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
374 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
375 when length(media_ids) > 0 do
378 |> Map.put("status", ".")
380 post_status(conn, params)
383 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
386 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
389 case get_req_header(conn, "idempotency-key") do
391 _ -> Ecto.UUID.generate()
395 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
398 |> put_view(StatusView)
399 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
402 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
403 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
409 |> json(%{error: "Can't delete this post"})
413 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
414 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
416 |> put_view(StatusView)
417 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
421 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
422 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
423 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
425 |> put_view(StatusView)
426 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
430 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
431 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
432 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
434 |> put_view(StatusView)
435 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
439 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
440 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
441 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
443 |> put_view(StatusView)
444 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
448 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
449 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
451 |> put_view(StatusView)
452 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
456 |> put_resp_content_type("application/json")
457 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
461 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
462 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
464 |> put_view(StatusView)
465 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
469 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
470 with %Activity{} = activity <- Repo.get(Activity, id),
471 %User{} = user <- User.get_by_nickname(user.nickname),
472 true <- Visibility.visible_for_user?(activity, user),
473 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
475 |> put_view(StatusView)
476 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
480 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
481 with %Activity{} = activity <- Repo.get(Activity, id),
482 %User{} = user <- User.get_by_nickname(user.nickname),
483 true <- Visibility.visible_for_user?(activity, user),
484 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
486 |> put_view(StatusView)
487 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
491 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
492 activity = Activity.get_by_id(id)
494 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
496 |> put_view(StatusView)
497 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
501 |> put_resp_content_type("application/json")
502 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
506 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
507 activity = Activity.get_by_id(id)
509 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
511 |> put_view(StatusView)
512 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
516 def notifications(%{assigns: %{user: user}} = conn, params) do
517 notifications = MastodonAPI.get_notifications(user, params)
520 |> add_link_headers(:notifications, notifications)
521 |> put_view(NotificationView)
522 |> render("index.json", %{notifications: notifications, for: user})
525 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
526 with {:ok, notification} <- Notification.get(user, id) do
528 |> put_view(NotificationView)
529 |> render("show.json", %{notification: notification, for: user})
533 |> put_resp_content_type("application/json")
534 |> send_resp(403, Jason.encode!(%{"error" => reason}))
538 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
539 Notification.clear(user)
543 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
544 with {:ok, _notif} <- Notification.dismiss(user, id) do
549 |> put_resp_content_type("application/json")
550 |> send_resp(403, Jason.encode!(%{"error" => reason}))
554 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
556 q = from(u in User, where: u.id in ^id)
557 targets = Repo.all(q)
560 |> put_view(AccountView)
561 |> render("relationships.json", %{user: user, targets: targets})
564 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
565 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
567 def update_media(%{assigns: %{user: user}} = conn, data) do
568 with %Object{} = object <- Repo.get(Object, data["id"]),
569 true <- Object.authorize_mutation(object, user),
570 true <- is_binary(data["description"]),
571 description <- data["description"] do
572 new_data = %{object.data | "name" => description}
576 |> Object.change(%{data: new_data})
579 attachment_data = Map.put(new_data, "id", object.id)
582 |> put_view(StatusView)
583 |> render("attachment.json", %{attachment: attachment_data})
587 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
588 with {:ok, object} <-
591 actor: User.ap_id(user),
592 description: Map.get(data, "description")
594 attachment_data = Map.put(object.data, "id", object.id)
597 |> put_view(StatusView)
598 |> render("attachment.json", %{attachment: attachment_data})
602 def favourited_by(conn, %{"id" => id}) do
603 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
604 q = from(u in User, where: u.ap_id in ^likes)
608 |> put_view(AccountView)
609 |> render(AccountView, "accounts.json", %{users: users, as: :user})
615 def reblogged_by(conn, %{"id" => id}) do
616 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
617 q = from(u in User, where: u.ap_id in ^announces)
621 |> put_view(AccountView)
622 |> render("accounts.json", %{users: users, as: :user})
628 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
629 local_only = params["local"] in [true, "True", "true", "1"]
632 [params["tag"], params["any"]]
636 |> Enum.map(&String.downcase(&1))
641 |> Enum.map(&String.downcase(&1))
646 |> Enum.map(&String.downcase(&1))
650 |> Map.put("type", "Create")
651 |> Map.put("local_only", local_only)
652 |> Map.put("blocking_user", user)
653 |> Map.put("muting_user", user)
654 |> Map.put("tag", tags)
655 |> Map.put("tag_all", tag_all)
656 |> Map.put("tag_reject", tag_reject)
657 |> ActivityPub.fetch_public_activities()
661 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
662 |> put_view(StatusView)
663 |> render("index.json", %{activities: activities, for: user, as: :activity})
666 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
667 with %User{} = user <- Repo.get(User, id),
668 followers <- MastodonAPI.get_followers(user, params) do
671 for_user && user.id == for_user.id -> followers
672 user.info.hide_followers -> []
677 |> add_link_headers(:followers, followers, user)
678 |> put_view(AccountView)
679 |> render("accounts.json", %{users: followers, as: :user})
683 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
684 with %User{} = user <- Repo.get(User, id),
685 followers <- MastodonAPI.get_friends(user, params) do
688 for_user && user.id == for_user.id -> followers
689 user.info.hide_follows -> []
694 |> add_link_headers(:following, followers, user)
695 |> put_view(AccountView)
696 |> render("accounts.json", %{users: followers, as: :user})
700 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
701 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
703 |> put_view(AccountView)
704 |> render("accounts.json", %{users: follow_requests, as: :user})
708 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
709 with %User{} = follower <- Repo.get(User, id),
710 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
712 |> put_view(AccountView)
713 |> render("relationship.json", %{user: followed, target: follower})
717 |> put_resp_content_type("application/json")
718 |> send_resp(403, Jason.encode!(%{"error" => message}))
722 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
723 with %User{} = follower <- Repo.get(User, id),
724 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
726 |> put_view(AccountView)
727 |> render("relationship.json", %{user: followed, target: follower})
731 |> put_resp_content_type("application/json")
732 |> send_resp(403, Jason.encode!(%{"error" => message}))
736 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
737 with %User{} = followed <- Repo.get(User, id),
738 false <- User.following?(follower, followed),
739 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
741 |> put_view(AccountView)
742 |> render("relationship.json", %{user: follower, target: followed})
745 followed = User.get_cached_by_id(id)
748 case conn.params["reblogs"] do
749 true -> CommonAPI.show_reblogs(follower, followed)
750 false -> CommonAPI.hide_reblogs(follower, followed)
754 |> put_view(AccountView)
755 |> render("relationship.json", %{user: follower, target: followed})
759 |> put_resp_content_type("application/json")
760 |> send_resp(403, Jason.encode!(%{"error" => message}))
764 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
765 with %User{} = followed <- Repo.get_by(User, nickname: uri),
766 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
768 |> put_view(AccountView)
769 |> render("account.json", %{user: followed, for: follower})
773 |> put_resp_content_type("application/json")
774 |> send_resp(403, Jason.encode!(%{"error" => message}))
778 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
779 with %User{} = followed <- Repo.get(User, id),
780 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
782 |> put_view(AccountView)
783 |> render("relationship.json", %{user: follower, target: followed})
787 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
788 with %User{} = muted <- Repo.get(User, id),
789 {:ok, muter} <- User.mute(muter, muted) do
791 |> put_view(AccountView)
792 |> render("relationship.json", %{user: muter, target: muted})
796 |> put_resp_content_type("application/json")
797 |> send_resp(403, Jason.encode!(%{"error" => message}))
801 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
802 with %User{} = muted <- Repo.get(User, id),
803 {:ok, muter} <- User.unmute(muter, muted) do
805 |> put_view(AccountView)
806 |> render("relationship.json", %{user: muter, target: muted})
810 |> put_resp_content_type("application/json")
811 |> send_resp(403, Jason.encode!(%{"error" => message}))
815 def mutes(%{assigns: %{user: user}} = conn, _) do
816 with muted_accounts <- User.muted_users(user) do
817 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
822 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
823 with %User{} = blocked <- Repo.get(User, id),
824 {:ok, blocker} <- User.block(blocker, blocked),
825 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: blocker, target: blocked})
832 |> put_resp_content_type("application/json")
833 |> send_resp(403, Jason.encode!(%{"error" => message}))
837 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
838 with %User{} = blocked <- Repo.get(User, id),
839 {:ok, blocker} <- User.unblock(blocker, blocked),
840 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: blocker, target: blocked})
847 |> put_resp_content_type("application/json")
848 |> send_resp(403, Jason.encode!(%{"error" => message}))
852 def blocks(%{assigns: %{user: user}} = conn, _) do
853 with blocked_accounts <- User.blocked_users(user) do
854 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
859 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
860 json(conn, info.domain_blocks || [])
863 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
864 User.block_domain(blocker, domain)
868 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
869 User.unblock_domain(blocker, domain)
873 def status_search(user, query) do
875 if Regex.match?(~r/https?:/, query) do
876 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
877 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
878 true <- Visibility.visible_for_user?(activity, user) do
888 where: fragment("?->>'type' = 'Create'", a.data),
889 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
892 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
897 order_by: [desc: :id]
900 Repo.all(q) ++ fetched
903 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
904 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
906 statuses = status_search(user, query)
908 tags_path = Web.base_url() <> "/tag/"
914 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
915 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
916 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
919 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
921 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
928 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
929 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
931 statuses = status_search(user, query)
937 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
938 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
941 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
943 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
950 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
951 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
953 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
958 def favourites(%{assigns: %{user: user}} = conn, params) do
961 |> Map.put("type", "Create")
962 |> Map.put("favorited_by", user.ap_id)
963 |> Map.put("blocking_user", user)
966 ActivityPub.fetch_activities([], params)
970 |> add_link_headers(:favourites, activities)
971 |> put_view(StatusView)
972 |> render("index.json", %{activities: activities, for: user, as: :activity})
975 def bookmarks(%{assigns: %{user: user}} = conn, _) do
976 user = Repo.get(User, user.id)
980 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
984 |> put_view(StatusView)
985 |> render("index.json", %{activities: activities, for: user, as: :activity})
988 def get_lists(%{assigns: %{user: user}} = conn, opts) do
989 lists = Pleroma.List.for_user(user, opts)
990 res = ListView.render("lists.json", lists: lists)
994 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
995 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
996 res = ListView.render("list.json", list: list)
1002 |> json(%{error: "Record not found"})
1006 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1007 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1008 res = ListView.render("lists.json", lists: lists)
1012 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1013 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1014 {:ok, _list} <- Pleroma.List.delete(list) do
1022 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1023 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1024 res = ListView.render("list.json", list: list)
1029 def add_to_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(User, account_id) do
1034 Pleroma.List.follow(list, followed)
1041 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1043 |> Enum.each(fn account_id ->
1044 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1045 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1046 Pleroma.List.unfollow(list, followed)
1053 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1054 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1055 {:ok, users} = Pleroma.List.get_following(list) do
1057 |> put_view(AccountView)
1058 |> render("accounts.json", %{users: users, as: :user})
1062 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1063 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1064 {:ok, list} <- Pleroma.List.rename(list, title) do
1065 res = ListView.render("list.json", list: list)
1073 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1074 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1077 |> Map.put("type", "Create")
1078 |> Map.put("blocking_user", user)
1079 |> Map.put("muting_user", user)
1081 # we must filter the following list for the user to avoid leaking statuses the user
1082 # does not actually have permission to see (for more info, peruse security issue #270).
1085 |> Enum.filter(fn x -> x in user.following end)
1086 |> ActivityPub.fetch_activities_bounded(following, params)
1090 |> put_view(StatusView)
1091 |> render("index.json", %{activities: activities, for: user, as: :activity})
1096 |> json(%{error: "Error."})
1100 def index(%{assigns: %{user: user}} = conn, _params) do
1103 |> get_session(:oauth_token)
1106 mastodon_emoji = mastodonized_emoji()
1108 limit = Config.get([:instance, :limit])
1111 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1113 flavour = get_user_flavour(user)
1118 streaming_api_base_url:
1119 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1120 access_token: token,
1122 domain: Pleroma.Web.Endpoint.host(),
1125 unfollow_modal: false,
1128 auto_play_gif: false,
1129 display_sensitive_media: false,
1130 reduce_motion: false,
1131 max_toot_chars: limit
1134 delete_others_notice: present?(user.info.is_moderator),
1135 admin: present?(user.info.is_admin)
1139 default_privacy: user.info.default_scope,
1140 default_sensitive: false,
1141 allow_content_types: Config.get([:instance, :allowed_post_formats])
1143 media_attachments: %{
1144 accept_content_types: [
1160 user.info.settings ||
1190 push_subscription: nil,
1192 custom_emojis: mastodon_emoji,
1198 |> put_layout(false)
1199 |> put_view(MastodonView)
1200 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1203 |> redirect(to: "/web/login")
1207 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1208 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1210 with changeset <- Ecto.Changeset.change(user),
1211 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1212 {:ok, _user} <- User.update_and_set_cache(changeset) do
1217 |> put_resp_content_type("application/json")
1218 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1222 @supported_flavours ["glitch", "vanilla"]
1224 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1225 when flavour in @supported_flavours do
1226 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1228 with changeset <- Ecto.Changeset.change(user),
1229 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1230 {:ok, user} <- User.update_and_set_cache(changeset),
1231 flavour <- user.info.flavour do
1236 |> put_resp_content_type("application/json")
1237 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1241 def set_flavour(conn, _params) do
1244 |> json(%{error: "Unsupported flavour"})
1247 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1248 json(conn, get_user_flavour(user))
1251 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1255 defp get_user_flavour(_) do
1259 def login(conn, %{"code" => code}) do
1260 with {:ok, app} <- get_or_make_app(),
1261 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1262 {:ok, token} <- Token.exchange_token(app, auth) do
1264 |> put_session(:oauth_token, token.token)
1265 |> redirect(to: "/web/getting-started")
1269 def login(conn, _) do
1270 with {:ok, app} <- get_or_make_app() do
1275 response_type: "code",
1276 client_id: app.client_id,
1278 scope: Enum.join(app.scopes, " ")
1282 |> redirect(to: path)
1286 defp get_or_make_app do
1287 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1288 scopes = ["read", "write", "follow", "push"]
1290 with %App{} = app <- Repo.get_by(App, find_attrs) do
1292 if app.scopes == scopes do
1296 |> Ecto.Changeset.change(%{scopes: scopes})
1304 App.register_changeset(
1306 Map.put(find_attrs, :scopes, scopes)
1313 def logout(conn, _) do
1316 |> redirect(to: "/")
1319 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1320 Logger.debug("Unimplemented, returning unmodified relationship")
1322 with %User{} = target <- Repo.get(User, id) do
1324 |> put_view(AccountView)
1325 |> render("relationship.json", %{user: user, target: target})
1329 def empty_array(conn, _) do
1330 Logger.debug("Unimplemented, returning an empty array")
1334 def empty_object(conn, _) do
1335 Logger.debug("Unimplemented, returning an empty object")
1339 def get_filters(%{assigns: %{user: user}} = conn, _) do
1340 filters = Filter.get_filters(user)
1341 res = FilterView.render("filters.json", filters: filters)
1346 %{assigns: %{user: user}} = conn,
1347 %{"phrase" => phrase, "context" => context} = params
1353 hide: Map.get(params, "irreversible", nil),
1354 whole_word: Map.get(params, "boolean", true)
1358 {:ok, response} = Filter.create(query)
1359 res = FilterView.render("filter.json", filter: response)
1363 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1364 filter = Filter.get(filter_id, user)
1365 res = FilterView.render("filter.json", filter: filter)
1370 %{assigns: %{user: user}} = conn,
1371 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1375 filter_id: filter_id,
1378 hide: Map.get(params, "irreversible", nil),
1379 whole_word: Map.get(params, "boolean", true)
1383 {:ok, response} = Filter.update(query)
1384 res = FilterView.render("filter.json", filter: response)
1388 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1391 filter_id: filter_id
1394 {:ok, _} = Filter.delete(query)
1400 def errors(conn, _) do
1403 |> json("Something went wrong")
1406 def suggestions(%{assigns: %{user: user}} = conn, _) do
1407 suggestions = Config.get(:suggestions)
1409 if Keyword.get(suggestions, :enabled, false) do
1410 api = Keyword.get(suggestions, :third_party_engine, "")
1411 timeout = Keyword.get(suggestions, :timeout, 5000)
1412 limit = Keyword.get(suggestions, :limit, 23)
1414 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1416 user = user.nickname
1420 |> String.replace("{{host}}", host)
1421 |> String.replace("{{user}}", user)
1423 with {:ok, %{status: 200, body: body}} <-
1428 recv_timeout: timeout,
1432 {:ok, data} <- Jason.decode(body) do
1435 |> Enum.slice(0, limit)
1440 case User.get_or_fetch(x["acct"]) do
1447 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1450 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1456 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1463 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1464 with %Activity{} = activity <- Repo.get(Activity, status_id),
1465 true <- Visibility.visible_for_user?(activity, user) do
1469 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1479 def reports(%{assigns: %{user: user}} = conn, params) do
1480 case CommonAPI.report(user, params) do
1483 |> put_view(ReportView)
1484 |> try_render("report.json", %{activity: activity})
1488 |> put_status(:bad_request)
1489 |> json(%{error: err})
1493 def try_render(conn, target, params)
1494 when is_binary(target) do
1495 res = render(conn, target, params)
1500 |> json(%{error: "Can't display this activity"})
1506 def try_render(conn, _, _) do
1509 |> json(%{error: "Can't display this activity"})
1512 defp present?(nil), do: false
1513 defp present?(false), do: false
1514 defp present?(_), do: true