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
13 alias Pleroma.Pagination
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.AppView
23 alias Pleroma.Web.MastodonAPI.FilterView
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonView
27 alias Pleroma.Web.MastodonAPI.NotificationView
28 alias Pleroma.Web.MastodonAPI.ReportView
29 alias Pleroma.Web.MastodonAPI.StatusView
30 alias Pleroma.Web.MediaProxy
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
58 |> render("show.json", %{app: app})
67 value_function \\ fn x -> {:ok, x} end
69 if Map.has_key?(params, params_field) do
70 case value_function.(params[params_field]) do
71 {:ok, new_value} -> Map.put(map, map_field, new_value)
79 def update_credentials(%{assigns: %{user: user}} = conn, params) do
84 |> add_if_present(params, "display_name", :name)
85 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
86 |> add_if_present(params, "avatar", :avatar, fn value ->
87 with %Plug.Upload{} <- value,
88 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
97 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
98 |> add_if_present(params, "header", :banner, fn value ->
99 with %Plug.Upload{} <- value,
100 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
107 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
109 with changeset <- User.update_changeset(user, user_params),
110 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
111 {:ok, user} <- User.update_and_set_cache(changeset) do
112 if original_user != user do
113 CommonAPI.update(user)
116 json(conn, AccountView.render("account.json", %{user: user, for: user}))
121 |> json(%{error: "Invalid request"})
125 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
126 account = AccountView.render("account.json", %{user: user, for: user})
130 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
131 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
134 |> render("short.json", %{app: app})
138 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
139 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
140 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
141 account = AccountView.render("account.json", %{user: user, for: for_user})
147 |> json(%{error: "Can't find user"})
151 @mastodon_api_level "2.5.0"
153 def masto_instance(conn, _params) do
154 instance = Config.get(:instance)
158 title: Keyword.get(instance, :name),
159 description: Keyword.get(instance, :description),
160 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
161 email: Keyword.get(instance, :email),
163 streaming_api: Pleroma.Web.Endpoint.websocket_url()
165 stats: Stats.get_stats(),
166 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
168 registrations: Pleroma.Config.get([:instance, :registrations_open]),
169 # Extra (not present in Mastodon):
170 max_toot_chars: Keyword.get(instance, :limit)
176 def peers(conn, _params) do
177 json(conn, Stats.get_peers())
180 defp mastodonized_emoji do
181 Pleroma.Emoji.get_all()
182 |> Enum.map(fn {shortcode, relative_url} ->
183 url = to_string(URI.merge(Web.base_url(), relative_url))
186 "shortcode" => shortcode,
188 "visible_in_picker" => true,
194 def custom_emojis(conn, _params) do
195 mastodon_emoji = mastodonized_emoji()
196 json(conn, mastodon_emoji)
199 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
202 |> Map.drop(["since_id", "max_id", "min_id"])
205 last = List.last(activities)
212 |> Map.get("limit", "20")
213 |> String.to_integer()
216 if length(activities) <= limit do
222 |> Enum.at(limit * -1)
226 {next_url, prev_url} =
230 Pleroma.Web.Endpoint,
233 Map.merge(params, %{max_id: max_id})
236 Pleroma.Web.Endpoint,
239 Map.merge(params, %{min_id: min_id})
245 Pleroma.Web.Endpoint,
247 Map.merge(params, %{max_id: max_id})
250 Pleroma.Web.Endpoint,
252 Map.merge(params, %{min_id: min_id})
258 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
264 def home_timeline(%{assigns: %{user: user}} = conn, params) do
267 |> Map.put("type", ["Create", "Announce"])
268 |> Map.put("blocking_user", user)
269 |> Map.put("muting_user", user)
270 |> Map.put("user", user)
273 [user.ap_id | user.following]
274 |> ActivityPub.fetch_activities(params)
275 |> ActivityPub.contain_timeline(user)
279 |> add_link_headers(:home_timeline, activities)
280 |> put_view(StatusView)
281 |> render("index.json", %{activities: activities, for: user, as: :activity})
284 def public_timeline(%{assigns: %{user: user}} = conn, params) do
285 local_only = params["local"] in [true, "True", "true", "1"]
289 |> Map.put("type", ["Create", "Announce"])
290 |> Map.put("local_only", local_only)
291 |> Map.put("blocking_user", user)
292 |> Map.put("muting_user", user)
293 |> ActivityPub.fetch_public_activities()
297 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
298 |> put_view(StatusView)
299 |> render("index.json", %{activities: activities, for: user, as: :activity})
302 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
303 with %User{} = user <- Repo.get(User, params["id"]) do
304 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
307 |> add_link_headers(:user_statuses, activities, params["id"])
308 |> put_view(StatusView)
309 |> render("index.json", %{
310 activities: activities,
317 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
320 |> Map.put("type", "Create")
321 |> Map.put("blocking_user", user)
322 |> Map.put("user", user)
323 |> Map.put(:visibility, "direct")
327 |> ActivityPub.fetch_activities_query(params)
328 |> Pagination.fetch_paginated(params)
331 |> add_link_headers(:dm_timeline, activities)
332 |> put_view(StatusView)
333 |> render("index.json", %{activities: activities, for: user, as: :activity})
336 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
337 with %Activity{} = activity <- Repo.get(Activity, id),
338 true <- Visibility.visible_for_user?(activity, user) do
340 |> put_view(StatusView)
341 |> try_render("status.json", %{activity: activity, for: user})
345 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
346 with %Activity{} = activity <- Repo.get(Activity, id),
348 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
349 "blocking_user" => user,
353 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
355 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
356 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
362 activities: grouped_activities[true] || [],
366 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
371 activities: grouped_activities[false] || [],
375 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
382 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
383 when length(media_ids) > 0 do
386 |> Map.put("status", ".")
388 post_status(conn, params)
391 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
394 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
397 case get_req_header(conn, "idempotency-key") do
399 _ -> Ecto.UUID.generate()
403 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
406 |> put_view(StatusView)
407 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
410 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
411 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
417 |> json(%{error: "Can't delete this post"})
421 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
422 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
424 |> put_view(StatusView)
425 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
429 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
430 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
431 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
433 |> put_view(StatusView)
434 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
438 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
439 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
440 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
442 |> put_view(StatusView)
443 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
447 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
448 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
449 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
451 |> put_view(StatusView)
452 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
456 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
457 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
459 |> put_view(StatusView)
460 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
464 |> put_resp_content_type("application/json")
465 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
469 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
470 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
472 |> put_view(StatusView)
473 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
477 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
478 with %Activity{} = activity <- Repo.get(Activity, id),
479 %User{} = user <- User.get_by_nickname(user.nickname),
480 true <- Visibility.visible_for_user?(activity, user),
481 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
483 |> put_view(StatusView)
484 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
488 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
489 with %Activity{} = activity <- Repo.get(Activity, id),
490 %User{} = user <- User.get_by_nickname(user.nickname),
491 true <- Visibility.visible_for_user?(activity, user),
492 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
494 |> put_view(StatusView)
495 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
499 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
500 activity = Activity.get_by_id(id)
502 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
504 |> put_view(StatusView)
505 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
509 |> put_resp_content_type("application/json")
510 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
514 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
515 activity = Activity.get_by_id(id)
517 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def notifications(%{assigns: %{user: user}} = conn, params) do
525 notifications = MastodonAPI.get_notifications(user, params)
528 |> add_link_headers(:notifications, notifications)
529 |> put_view(NotificationView)
530 |> render("index.json", %{notifications: notifications, for: user})
533 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
534 with {:ok, notification} <- Notification.get(user, id) do
536 |> put_view(NotificationView)
537 |> render("show.json", %{notification: notification, for: user})
541 |> put_resp_content_type("application/json")
542 |> send_resp(403, Jason.encode!(%{"error" => reason}))
546 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
547 Notification.clear(user)
551 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
552 with {:ok, _notif} <- Notification.dismiss(user, id) do
557 |> put_resp_content_type("application/json")
558 |> send_resp(403, Jason.encode!(%{"error" => reason}))
562 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
564 q = from(u in User, where: u.id in ^id)
565 targets = Repo.all(q)
568 |> put_view(AccountView)
569 |> render("relationships.json", %{user: user, targets: targets})
572 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
573 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
575 def update_media(%{assigns: %{user: user}} = conn, data) do
576 with %Object{} = object <- Repo.get(Object, data["id"]),
577 true <- Object.authorize_mutation(object, user),
578 true <- is_binary(data["description"]),
579 description <- data["description"] do
580 new_data = %{object.data | "name" => description}
584 |> Object.change(%{data: new_data})
587 attachment_data = Map.put(new_data, "id", object.id)
590 |> put_view(StatusView)
591 |> render("attachment.json", %{attachment: attachment_data})
595 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
596 with {:ok, object} <-
599 actor: User.ap_id(user),
600 description: Map.get(data, "description")
602 attachment_data = Map.put(object.data, "id", object.id)
605 |> put_view(StatusView)
606 |> render("attachment.json", %{attachment: attachment_data})
610 def favourited_by(conn, %{"id" => id}) do
611 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
612 q = from(u in User, where: u.ap_id in ^likes)
616 |> put_view(AccountView)
617 |> render(AccountView, "accounts.json", %{users: users, as: :user})
623 def reblogged_by(conn, %{"id" => id}) do
624 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
625 q = from(u in User, where: u.ap_id in ^announces)
629 |> put_view(AccountView)
630 |> render("accounts.json", %{users: users, as: :user})
636 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
637 local_only = params["local"] in [true, "True", "true", "1"]
640 [params["tag"], params["any"]]
644 |> Enum.map(&String.downcase(&1))
649 |> Enum.map(&String.downcase(&1))
654 |> Enum.map(&String.downcase(&1))
658 |> Map.put("type", "Create")
659 |> Map.put("local_only", local_only)
660 |> Map.put("blocking_user", user)
661 |> Map.put("muting_user", user)
662 |> Map.put("tag", tags)
663 |> Map.put("tag_all", tag_all)
664 |> Map.put("tag_reject", tag_reject)
665 |> ActivityPub.fetch_public_activities()
669 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
670 |> put_view(StatusView)
671 |> render("index.json", %{activities: activities, for: user, as: :activity})
674 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
675 with %User{} = user <- Repo.get(User, id),
676 followers <- MastodonAPI.get_followers(user, params) do
679 for_user && user.id == for_user.id -> followers
680 user.info.hide_followers -> []
685 |> add_link_headers(:followers, followers, user)
686 |> put_view(AccountView)
687 |> render("accounts.json", %{users: followers, as: :user})
691 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
692 with %User{} = user <- Repo.get(User, id),
693 followers <- MastodonAPI.get_friends(user, params) do
696 for_user && user.id == for_user.id -> followers
697 user.info.hide_follows -> []
702 |> add_link_headers(:following, followers, user)
703 |> put_view(AccountView)
704 |> render("accounts.json", %{users: followers, as: :user})
708 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
709 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
711 |> put_view(AccountView)
712 |> render("accounts.json", %{users: follow_requests, as: :user})
716 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
717 with %User{} = follower <- Repo.get(User, id),
718 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
720 |> put_view(AccountView)
721 |> render("relationship.json", %{user: followed, target: follower})
725 |> put_resp_content_type("application/json")
726 |> send_resp(403, Jason.encode!(%{"error" => message}))
730 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
731 with %User{} = follower <- Repo.get(User, id),
732 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
734 |> put_view(AccountView)
735 |> render("relationship.json", %{user: followed, target: follower})
739 |> put_resp_content_type("application/json")
740 |> send_resp(403, Jason.encode!(%{"error" => message}))
744 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
745 with %User{} = followed <- Repo.get(User, id),
746 false <- User.following?(follower, followed),
747 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
749 |> put_view(AccountView)
750 |> render("relationship.json", %{user: follower, target: followed})
753 followed = User.get_cached_by_id(id)
756 case conn.params["reblogs"] do
757 true -> CommonAPI.show_reblogs(follower, followed)
758 false -> CommonAPI.hide_reblogs(follower, followed)
762 |> put_view(AccountView)
763 |> render("relationship.json", %{user: follower, target: followed})
767 |> put_resp_content_type("application/json")
768 |> send_resp(403, Jason.encode!(%{"error" => message}))
772 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
773 with %User{} = followed <- Repo.get_by(User, nickname: uri),
774 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
776 |> put_view(AccountView)
777 |> render("account.json", %{user: followed, for: follower})
781 |> put_resp_content_type("application/json")
782 |> send_resp(403, Jason.encode!(%{"error" => message}))
786 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
787 with %User{} = followed <- Repo.get(User, id),
788 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
790 |> put_view(AccountView)
791 |> render("relationship.json", %{user: follower, target: followed})
795 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
796 with %User{} = muted <- Repo.get(User, id),
797 {:ok, muter} <- User.mute(muter, muted) do
799 |> put_view(AccountView)
800 |> render("relationship.json", %{user: muter, target: muted})
804 |> put_resp_content_type("application/json")
805 |> send_resp(403, Jason.encode!(%{"error" => message}))
809 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
810 with %User{} = muted <- Repo.get(User, id),
811 {:ok, muter} <- User.unmute(muter, muted) do
813 |> put_view(AccountView)
814 |> render("relationship.json", %{user: muter, target: muted})
818 |> put_resp_content_type("application/json")
819 |> send_resp(403, Jason.encode!(%{"error" => message}))
823 def mutes(%{assigns: %{user: user}} = conn, _) do
824 with muted_accounts <- User.muted_users(user) do
825 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
830 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
831 with %User{} = blocked <- Repo.get(User, id),
832 {:ok, blocker} <- User.block(blocker, blocked),
833 {:ok, _activity} <- ActivityPub.block(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 unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
846 with %User{} = blocked <- Repo.get(User, id),
847 {:ok, blocker} <- User.unblock(blocker, blocked),
848 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
850 |> put_view(AccountView)
851 |> render("relationship.json", %{user: blocker, target: blocked})
855 |> put_resp_content_type("application/json")
856 |> send_resp(403, Jason.encode!(%{"error" => message}))
860 def blocks(%{assigns: %{user: user}} = conn, _) do
861 with blocked_accounts <- User.blocked_users(user) do
862 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
867 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
868 json(conn, info.domain_blocks || [])
871 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
872 User.block_domain(blocker, domain)
876 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
877 User.unblock_domain(blocker, domain)
881 def status_search(user, query) do
883 if Regex.match?(~r/https?:/, query) do
884 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
885 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
886 true <- Visibility.visible_for_user?(activity, user) do
896 where: fragment("?->>'type' = 'Create'", a.data),
897 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
900 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
905 order_by: [desc: :id]
908 Repo.all(q) ++ fetched
911 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
912 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
914 statuses = status_search(user, query)
916 tags_path = Web.base_url() <> "/tag/"
922 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
923 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
924 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
927 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
929 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
936 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
937 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
939 statuses = status_search(user, query)
945 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
946 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
949 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
951 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
958 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
959 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
961 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
966 def favourites(%{assigns: %{user: user}} = conn, params) do
969 |> Map.put("type", "Create")
970 |> Map.put("favorited_by", user.ap_id)
971 |> Map.put("blocking_user", user)
974 ActivityPub.fetch_activities([], params)
978 |> add_link_headers(:favourites, activities)
979 |> put_view(StatusView)
980 |> render("index.json", %{activities: activities, for: user, as: :activity})
983 def bookmarks(%{assigns: %{user: user}} = conn, _) do
984 user = Repo.get(User, user.id)
988 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
992 |> put_view(StatusView)
993 |> render("index.json", %{activities: activities, for: user, as: :activity})
996 def get_lists(%{assigns: %{user: user}} = conn, opts) do
997 lists = Pleroma.List.for_user(user, opts)
998 res = ListView.render("lists.json", lists: lists)
1002 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1003 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1004 res = ListView.render("list.json", list: list)
1010 |> json(%{error: "Record not found"})
1014 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1015 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1016 res = ListView.render("lists.json", lists: lists)
1020 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1021 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1022 {:ok, _list} <- Pleroma.List.delete(list) do
1030 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1031 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1032 res = ListView.render("list.json", list: list)
1037 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1039 |> Enum.each(fn account_id ->
1040 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1041 %User{} = followed <- Repo.get(User, account_id) do
1042 Pleroma.List.follow(list, followed)
1049 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1051 |> Enum.each(fn account_id ->
1052 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1053 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1054 Pleroma.List.unfollow(list, followed)
1061 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1062 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1063 {:ok, users} = Pleroma.List.get_following(list) do
1065 |> put_view(AccountView)
1066 |> render("accounts.json", %{users: users, as: :user})
1070 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1071 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1072 {:ok, list} <- Pleroma.List.rename(list, title) do
1073 res = ListView.render("list.json", list: list)
1081 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1082 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1085 |> Map.put("type", "Create")
1086 |> Map.put("blocking_user", user)
1087 |> Map.put("muting_user", user)
1089 # we must filter the following list for the user to avoid leaking statuses the user
1090 # does not actually have permission to see (for more info, peruse security issue #270).
1093 |> Enum.filter(fn x -> x in user.following end)
1094 |> ActivityPub.fetch_activities_bounded(following, params)
1098 |> put_view(StatusView)
1099 |> render("index.json", %{activities: activities, for: user, as: :activity})
1104 |> json(%{error: "Error."})
1108 def index(%{assigns: %{user: user}} = conn, _params) do
1111 |> get_session(:oauth_token)
1114 mastodon_emoji = mastodonized_emoji()
1116 limit = Config.get([:instance, :limit])
1119 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1121 flavour = get_user_flavour(user)
1126 streaming_api_base_url:
1127 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1128 access_token: token,
1130 domain: Pleroma.Web.Endpoint.host(),
1133 unfollow_modal: false,
1136 auto_play_gif: false,
1137 display_sensitive_media: false,
1138 reduce_motion: false,
1139 max_toot_chars: limit
1142 delete_others_notice: present?(user.info.is_moderator),
1143 admin: present?(user.info.is_admin)
1147 default_privacy: user.info.default_scope,
1148 default_sensitive: false,
1149 allow_content_types: Config.get([:instance, :allowed_post_formats])
1151 media_attachments: %{
1152 accept_content_types: [
1168 user.info.settings ||
1198 push_subscription: nil,
1200 custom_emojis: mastodon_emoji,
1206 |> put_layout(false)
1207 |> put_view(MastodonView)
1208 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1211 |> redirect(to: "/web/login")
1215 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1216 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1218 with changeset <- Ecto.Changeset.change(user),
1219 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1220 {:ok, _user} <- User.update_and_set_cache(changeset) do
1225 |> put_resp_content_type("application/json")
1226 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1230 @supported_flavours ["glitch", "vanilla"]
1232 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1233 when flavour in @supported_flavours do
1234 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1236 with changeset <- Ecto.Changeset.change(user),
1237 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1238 {:ok, user} <- User.update_and_set_cache(changeset),
1239 flavour <- user.info.flavour do
1244 |> put_resp_content_type("application/json")
1245 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1249 def set_flavour(conn, _params) do
1252 |> json(%{error: "Unsupported flavour"})
1255 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1256 json(conn, get_user_flavour(user))
1259 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1263 defp get_user_flavour(_) do
1267 def login(conn, %{"code" => code}) do
1268 with {:ok, app} <- get_or_make_app(),
1269 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1270 {:ok, token} <- Token.exchange_token(app, auth) do
1272 |> put_session(:oauth_token, token.token)
1273 |> redirect(to: "/web/getting-started")
1277 def login(conn, _) do
1278 with {:ok, app} <- get_or_make_app() do
1283 response_type: "code",
1284 client_id: app.client_id,
1286 scope: Enum.join(app.scopes, " ")
1290 |> redirect(to: path)
1294 defp get_or_make_app do
1295 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1296 scopes = ["read", "write", "follow", "push"]
1298 with %App{} = app <- Repo.get_by(App, find_attrs) do
1300 if app.scopes == scopes do
1304 |> Ecto.Changeset.change(%{scopes: scopes})
1312 App.register_changeset(
1314 Map.put(find_attrs, :scopes, scopes)
1321 def logout(conn, _) do
1324 |> redirect(to: "/")
1327 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1328 Logger.debug("Unimplemented, returning unmodified relationship")
1330 with %User{} = target <- Repo.get(User, id) do
1332 |> put_view(AccountView)
1333 |> render("relationship.json", %{user: user, target: target})
1337 def empty_array(conn, _) do
1338 Logger.debug("Unimplemented, returning an empty array")
1342 def empty_object(conn, _) do
1343 Logger.debug("Unimplemented, returning an empty object")
1347 def get_filters(%{assigns: %{user: user}} = conn, _) do
1348 filters = Filter.get_filters(user)
1349 res = FilterView.render("filters.json", filters: filters)
1354 %{assigns: %{user: user}} = conn,
1355 %{"phrase" => phrase, "context" => context} = params
1361 hide: Map.get(params, "irreversible", nil),
1362 whole_word: Map.get(params, "boolean", true)
1366 {:ok, response} = Filter.create(query)
1367 res = FilterView.render("filter.json", filter: response)
1371 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1372 filter = Filter.get(filter_id, user)
1373 res = FilterView.render("filter.json", filter: filter)
1378 %{assigns: %{user: user}} = conn,
1379 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1383 filter_id: filter_id,
1386 hide: Map.get(params, "irreversible", nil),
1387 whole_word: Map.get(params, "boolean", true)
1391 {:ok, response} = Filter.update(query)
1392 res = FilterView.render("filter.json", filter: response)
1396 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1399 filter_id: filter_id
1402 {:ok, _} = Filter.delete(query)
1408 def errors(conn, _) do
1411 |> json("Something went wrong")
1414 def suggestions(%{assigns: %{user: user}} = conn, _) do
1415 suggestions = Config.get(:suggestions)
1417 if Keyword.get(suggestions, :enabled, false) do
1418 api = Keyword.get(suggestions, :third_party_engine, "")
1419 timeout = Keyword.get(suggestions, :timeout, 5000)
1420 limit = Keyword.get(suggestions, :limit, 23)
1422 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1424 user = user.nickname
1428 |> String.replace("{{host}}", host)
1429 |> String.replace("{{user}}", user)
1431 with {:ok, %{status: 200, body: body}} <-
1436 recv_timeout: timeout,
1440 {:ok, data} <- Jason.decode(body) do
1443 |> Enum.slice(0, limit)
1448 case User.get_or_fetch(x["acct"]) do
1455 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1458 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1464 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1471 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1472 with %Activity{} = activity <- Repo.get(Activity, status_id),
1473 true <- Visibility.visible_for_user?(activity, user) do
1477 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1487 def reports(%{assigns: %{user: user}} = conn, params) do
1488 case CommonAPI.report(user, params) do
1491 |> put_view(ReportView)
1492 |> try_render("report.json", %{activity: activity})
1496 |> put_status(:bad_request)
1497 |> json(%{error: err})
1501 def try_render(conn, target, params)
1502 when is_binary(target) do
1503 res = render(conn, target, params)
1508 |> json(%{error: "Can't display this activity"})
1514 def try_render(conn, _, _) do
1517 |> json(%{error: "Can't display this activity"})
1520 defp present?(nil), do: false
1521 defp present?(false), do: false
1522 defp present?(_), do: true