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()
215 min_id = Enum.at(activities, limit * -1)
217 {next_url, prev_url} =
221 Pleroma.Web.Endpoint,
224 Map.merge(params, %{max_id: max_id})
227 Pleroma.Web.Endpoint,
230 Map.merge(params, %{min_id: min_id})
236 Pleroma.Web.Endpoint,
238 Map.merge(params, %{max_id: max_id})
241 Pleroma.Web.Endpoint,
243 Map.merge(params, %{min_id: min_id})
249 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
255 def home_timeline(%{assigns: %{user: user}} = conn, params) do
258 |> Map.put("type", ["Create", "Announce"])
259 |> Map.put("blocking_user", user)
260 |> Map.put("muting_user", user)
261 |> Map.put("user", user)
264 [user.ap_id | user.following]
265 |> ActivityPub.fetch_activities(params)
266 |> ActivityPub.contain_timeline(user)
270 |> add_link_headers(:home_timeline, activities)
271 |> put_view(StatusView)
272 |> render("index.json", %{activities: activities, for: user, as: :activity})
275 def public_timeline(%{assigns: %{user: user}} = conn, params) do
276 local_only = params["local"] in [true, "True", "true", "1"]
280 |> Map.put("type", ["Create", "Announce"])
281 |> Map.put("local_only", local_only)
282 |> Map.put("blocking_user", user)
283 |> Map.put("muting_user", user)
284 |> ActivityPub.fetch_public_activities()
288 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
289 |> put_view(StatusView)
290 |> render("index.json", %{activities: activities, for: user, as: :activity})
293 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
294 with %User{} = user <- Repo.get(User, params["id"]) do
295 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
298 |> add_link_headers(:user_statuses, activities, params["id"])
299 |> put_view(StatusView)
300 |> render("index.json", %{
301 activities: activities,
308 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
311 |> Map.put("type", "Create")
312 |> Map.put("blocking_user", user)
313 |> Map.put("user", user)
314 |> Map.put(:visibility, "direct")
318 |> ActivityPub.fetch_activities_query(params)
319 |> Pagination.fetch_paginated(params)
322 |> add_link_headers(:dm_timeline, activities)
323 |> put_view(StatusView)
324 |> render("index.json", %{activities: activities, for: user, as: :activity})
327 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
328 with %Activity{} = activity <- Repo.get(Activity, id),
329 true <- Visibility.visible_for_user?(activity, user) do
331 |> put_view(StatusView)
332 |> try_render("status.json", %{activity: activity, for: user})
336 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
337 with %Activity{} = activity <- Repo.get(Activity, id),
339 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
340 "blocking_user" => user,
344 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
346 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
347 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
353 activities: grouped_activities[true] || [],
357 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
362 activities: grouped_activities[false] || [],
366 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
373 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
374 when length(media_ids) > 0 do
377 |> Map.put("status", ".")
379 post_status(conn, params)
382 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
385 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
388 case get_req_header(conn, "idempotency-key") do
390 _ -> Ecto.UUID.generate()
394 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
397 |> put_view(StatusView)
398 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
401 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
402 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
408 |> json(%{error: "Can't delete this post"})
412 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
413 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
415 |> put_view(StatusView)
416 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
420 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
421 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
422 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
424 |> put_view(StatusView)
425 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
429 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
430 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(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 unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
439 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(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 pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
448 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
450 |> put_view(StatusView)
451 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
455 |> put_resp_content_type("application/json")
456 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
460 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
461 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
463 |> put_view(StatusView)
464 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
468 def bookmark_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.bookmark(user, activity.data["object"]["id"]) do
474 |> put_view(StatusView)
475 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
479 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 with %Activity{} = activity <- Repo.get(Activity, id),
481 %User{} = user <- User.get_by_nickname(user.nickname),
482 true <- Visibility.visible_for_user?(activity, user),
483 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
485 |> put_view(StatusView)
486 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
490 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
491 activity = Activity.get_by_id(id)
493 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
495 |> put_view(StatusView)
496 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
500 |> put_resp_content_type("application/json")
501 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
505 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
506 activity = Activity.get_by_id(id)
508 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
515 def notifications(%{assigns: %{user: user}} = conn, params) do
516 notifications = MastodonAPI.get_notifications(user, params)
519 |> add_link_headers(:notifications, notifications)
520 |> put_view(NotificationView)
521 |> render("index.json", %{notifications: notifications, for: user})
524 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
525 with {:ok, notification} <- Notification.get(user, id) do
527 |> put_view(NotificationView)
528 |> render("show.json", %{notification: notification, for: user})
532 |> put_resp_content_type("application/json")
533 |> send_resp(403, Jason.encode!(%{"error" => reason}))
537 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
538 Notification.clear(user)
542 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
543 with {:ok, _notif} <- Notification.dismiss(user, id) do
548 |> put_resp_content_type("application/json")
549 |> send_resp(403, Jason.encode!(%{"error" => reason}))
553 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
555 q = from(u in User, where: u.id in ^id)
556 targets = Repo.all(q)
559 |> put_view(AccountView)
560 |> render("relationships.json", %{user: user, targets: targets})
563 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
564 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
566 def update_media(%{assigns: %{user: user}} = conn, data) do
567 with %Object{} = object <- Repo.get(Object, data["id"]),
568 true <- Object.authorize_mutation(object, user),
569 true <- is_binary(data["description"]),
570 description <- data["description"] do
571 new_data = %{object.data | "name" => description}
575 |> Object.change(%{data: new_data})
578 attachment_data = Map.put(new_data, "id", object.id)
581 |> put_view(StatusView)
582 |> render("attachment.json", %{attachment: attachment_data})
586 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
587 with {:ok, object} <-
590 actor: User.ap_id(user),
591 description: Map.get(data, "description")
593 attachment_data = Map.put(object.data, "id", object.id)
596 |> put_view(StatusView)
597 |> render("attachment.json", %{attachment: attachment_data})
601 def favourited_by(conn, %{"id" => id}) do
602 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
603 q = from(u in User, where: u.ap_id in ^likes)
607 |> put_view(AccountView)
608 |> render(AccountView, "accounts.json", %{users: users, as: :user})
614 def reblogged_by(conn, %{"id" => id}) do
615 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
616 q = from(u in User, where: u.ap_id in ^announces)
620 |> put_view(AccountView)
621 |> render("accounts.json", %{users: users, as: :user})
627 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
628 local_only = params["local"] in [true, "True", "true", "1"]
631 [params["tag"], params["any"]]
635 |> Enum.map(&String.downcase(&1))
640 |> Enum.map(&String.downcase(&1))
645 |> Enum.map(&String.downcase(&1))
649 |> Map.put("type", "Create")
650 |> Map.put("local_only", local_only)
651 |> Map.put("blocking_user", user)
652 |> Map.put("muting_user", user)
653 |> Map.put("tag", tags)
654 |> Map.put("tag_all", tag_all)
655 |> Map.put("tag_reject", tag_reject)
656 |> ActivityPub.fetch_public_activities()
660 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
661 |> put_view(StatusView)
662 |> render("index.json", %{activities: activities, for: user, as: :activity})
665 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
666 with %User{} = user <- Repo.get(User, id),
667 followers <- MastodonAPI.get_followers(user, params) do
670 for_user && user.id == for_user.id -> followers
671 user.info.hide_followers -> []
676 |> add_link_headers(:followers, followers, user)
677 |> put_view(AccountView)
678 |> render("accounts.json", %{users: followers, as: :user})
682 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
683 with %User{} = user <- Repo.get(User, id),
684 followers <- MastodonAPI.get_friends(user, params) do
687 for_user && user.id == for_user.id -> followers
688 user.info.hide_follows -> []
693 |> add_link_headers(:following, followers, user)
694 |> put_view(AccountView)
695 |> render("accounts.json", %{users: followers, as: :user})
699 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
700 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
702 |> put_view(AccountView)
703 |> render("accounts.json", %{users: follow_requests, as: :user})
707 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
708 with %User{} = follower <- Repo.get(User, id),
709 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
711 |> put_view(AccountView)
712 |> render("relationship.json", %{user: followed, target: follower})
716 |> put_resp_content_type("application/json")
717 |> send_resp(403, Jason.encode!(%{"error" => message}))
721 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
722 with %User{} = follower <- Repo.get(User, id),
723 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
725 |> put_view(AccountView)
726 |> render("relationship.json", %{user: followed, target: follower})
730 |> put_resp_content_type("application/json")
731 |> send_resp(403, Jason.encode!(%{"error" => message}))
735 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
736 with %User{} = followed <- Repo.get(User, id),
737 false <- User.following?(follower, followed),
738 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
740 |> put_view(AccountView)
741 |> render("relationship.json", %{user: follower, target: followed})
744 followed = User.get_cached_by_id(id)
747 case conn.params["reblogs"] do
748 true -> CommonAPI.show_reblogs(follower, followed)
749 false -> CommonAPI.hide_reblogs(follower, followed)
753 |> put_view(AccountView)
754 |> render("relationship.json", %{user: follower, target: followed})
758 |> put_resp_content_type("application/json")
759 |> send_resp(403, Jason.encode!(%{"error" => message}))
763 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
764 with %User{} = followed <- Repo.get_by(User, nickname: uri),
765 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
767 |> put_view(AccountView)
768 |> render("account.json", %{user: followed, for: follower})
772 |> put_resp_content_type("application/json")
773 |> send_resp(403, Jason.encode!(%{"error" => message}))
777 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
778 with %User{} = followed <- Repo.get(User, id),
779 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
781 |> put_view(AccountView)
782 |> render("relationship.json", %{user: follower, target: followed})
786 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
787 with %User{} = muted <- Repo.get(User, id),
788 {:ok, muter} <- User.mute(muter, muted) do
790 |> put_view(AccountView)
791 |> render("relationship.json", %{user: muter, target: muted})
795 |> put_resp_content_type("application/json")
796 |> send_resp(403, Jason.encode!(%{"error" => message}))
800 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
801 with %User{} = muted <- Repo.get(User, id),
802 {:ok, muter} <- User.unmute(muter, muted) do
804 |> put_view(AccountView)
805 |> render("relationship.json", %{user: muter, target: muted})
809 |> put_resp_content_type("application/json")
810 |> send_resp(403, Jason.encode!(%{"error" => message}))
814 def mutes(%{assigns: %{user: user}} = conn, _) do
815 with muted_accounts <- User.muted_users(user) do
816 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
821 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
822 with %User{} = blocked <- Repo.get(User, id),
823 {:ok, blocker} <- User.block(blocker, blocked),
824 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
826 |> put_view(AccountView)
827 |> render("relationship.json", %{user: blocker, target: blocked})
831 |> put_resp_content_type("application/json")
832 |> send_resp(403, Jason.encode!(%{"error" => message}))
836 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
837 with %User{} = blocked <- Repo.get(User, id),
838 {:ok, blocker} <- User.unblock(blocker, blocked),
839 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
841 |> put_view(AccountView)
842 |> render("relationship.json", %{user: blocker, target: blocked})
846 |> put_resp_content_type("application/json")
847 |> send_resp(403, Jason.encode!(%{"error" => message}))
851 def blocks(%{assigns: %{user: user}} = conn, _) do
852 with blocked_accounts <- User.blocked_users(user) do
853 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
858 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
859 json(conn, info.domain_blocks || [])
862 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
863 User.block_domain(blocker, domain)
867 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
868 User.unblock_domain(blocker, domain)
872 def status_search(user, query) do
874 if Regex.match?(~r/https?:/, query) do
875 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
876 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
877 true <- Visibility.visible_for_user?(activity, user) do
887 where: fragment("?->>'type' = 'Create'", a.data),
888 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
891 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
896 order_by: [desc: :id]
899 Repo.all(q) ++ fetched
902 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
903 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
905 statuses = status_search(user, query)
907 tags_path = Web.base_url() <> "/tag/"
913 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
914 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
915 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
918 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
920 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
927 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
928 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
930 statuses = status_search(user, query)
936 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
937 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
940 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
942 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
949 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
950 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
952 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
957 def favourites(%{assigns: %{user: user}} = conn, params) do
960 |> Map.put("type", "Create")
961 |> Map.put("favorited_by", user.ap_id)
962 |> Map.put("blocking_user", user)
965 ActivityPub.fetch_activities([], params)
969 |> add_link_headers(:favourites, activities)
970 |> put_view(StatusView)
971 |> render("index.json", %{activities: activities, for: user, as: :activity})
974 def bookmarks(%{assigns: %{user: user}} = conn, _) do
975 user = Repo.get(User, user.id)
979 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
983 |> put_view(StatusView)
984 |> render("index.json", %{activities: activities, for: user, as: :activity})
987 def get_lists(%{assigns: %{user: user}} = conn, opts) do
988 lists = Pleroma.List.for_user(user, opts)
989 res = ListView.render("lists.json", lists: lists)
993 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
994 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
995 res = ListView.render("list.json", list: list)
1001 |> json(%{error: "Record not found"})
1005 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1006 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1007 res = ListView.render("lists.json", lists: lists)
1011 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1012 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1013 {:ok, _list} <- Pleroma.List.delete(list) do
1021 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1022 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1023 res = ListView.render("list.json", list: list)
1028 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1030 |> Enum.each(fn account_id ->
1031 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1032 %User{} = followed <- Repo.get(User, account_id) do
1033 Pleroma.List.follow(list, followed)
1040 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1042 |> Enum.each(fn account_id ->
1043 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1044 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1045 Pleroma.List.unfollow(list, followed)
1052 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1053 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1054 {:ok, users} = Pleroma.List.get_following(list) do
1056 |> put_view(AccountView)
1057 |> render("accounts.json", %{users: users, as: :user})
1061 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1062 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1063 {:ok, list} <- Pleroma.List.rename(list, title) do
1064 res = ListView.render("list.json", list: list)
1072 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1073 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1076 |> Map.put("type", "Create")
1077 |> Map.put("blocking_user", user)
1078 |> Map.put("muting_user", user)
1080 # we must filter the following list for the user to avoid leaking statuses the user
1081 # does not actually have permission to see (for more info, peruse security issue #270).
1084 |> Enum.filter(fn x -> x in user.following end)
1085 |> ActivityPub.fetch_activities_bounded(following, params)
1089 |> put_view(StatusView)
1090 |> render("index.json", %{activities: activities, for: user, as: :activity})
1095 |> json(%{error: "Error."})
1099 def index(%{assigns: %{user: user}} = conn, _params) do
1102 |> get_session(:oauth_token)
1105 mastodon_emoji = mastodonized_emoji()
1107 limit = Config.get([:instance, :limit])
1110 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1112 flavour = get_user_flavour(user)
1117 streaming_api_base_url:
1118 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1119 access_token: token,
1121 domain: Pleroma.Web.Endpoint.host(),
1124 unfollow_modal: false,
1127 auto_play_gif: false,
1128 display_sensitive_media: false,
1129 reduce_motion: false,
1130 max_toot_chars: limit
1133 delete_others_notice: present?(user.info.is_moderator),
1134 admin: present?(user.info.is_admin)
1138 default_privacy: user.info.default_scope,
1139 default_sensitive: false,
1140 allow_content_types: Config.get([:instance, :allowed_post_formats])
1142 media_attachments: %{
1143 accept_content_types: [
1159 user.info.settings ||
1189 push_subscription: nil,
1191 custom_emojis: mastodon_emoji,
1197 |> put_layout(false)
1198 |> put_view(MastodonView)
1199 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1202 |> redirect(to: "/web/login")
1206 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1207 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1209 with changeset <- Ecto.Changeset.change(user),
1210 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1211 {:ok, _user} <- User.update_and_set_cache(changeset) do
1216 |> put_resp_content_type("application/json")
1217 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1221 @supported_flavours ["glitch", "vanilla"]
1223 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1224 when flavour in @supported_flavours do
1225 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1227 with changeset <- Ecto.Changeset.change(user),
1228 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1229 {:ok, user} <- User.update_and_set_cache(changeset),
1230 flavour <- user.info.flavour do
1235 |> put_resp_content_type("application/json")
1236 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1240 def set_flavour(conn, _params) do
1243 |> json(%{error: "Unsupported flavour"})
1246 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1247 json(conn, get_user_flavour(user))
1250 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1254 defp get_user_flavour(_) do
1258 def login(conn, %{"code" => code}) do
1259 with {:ok, app} <- get_or_make_app(),
1260 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1261 {:ok, token} <- Token.exchange_token(app, auth) do
1263 |> put_session(:oauth_token, token.token)
1264 |> redirect(to: "/web/getting-started")
1268 def login(conn, _) do
1269 with {:ok, app} <- get_or_make_app() do
1274 response_type: "code",
1275 client_id: app.client_id,
1277 scope: Enum.join(app.scopes, " ")
1281 |> redirect(to: path)
1285 defp get_or_make_app do
1286 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1287 scopes = ["read", "write", "follow", "push"]
1289 with %App{} = app <- Repo.get_by(App, find_attrs) do
1291 if app.scopes == scopes do
1295 |> Ecto.Changeset.change(%{scopes: scopes})
1303 App.register_changeset(
1305 Map.put(find_attrs, :scopes, scopes)
1312 def logout(conn, _) do
1315 |> redirect(to: "/")
1318 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1319 Logger.debug("Unimplemented, returning unmodified relationship")
1321 with %User{} = target <- Repo.get(User, id) do
1323 |> put_view(AccountView)
1324 |> render("relationship.json", %{user: user, target: target})
1328 def empty_array(conn, _) do
1329 Logger.debug("Unimplemented, returning an empty array")
1333 def empty_object(conn, _) do
1334 Logger.debug("Unimplemented, returning an empty object")
1338 def get_filters(%{assigns: %{user: user}} = conn, _) do
1339 filters = Filter.get_filters(user)
1340 res = FilterView.render("filters.json", filters: filters)
1345 %{assigns: %{user: user}} = conn,
1346 %{"phrase" => phrase, "context" => context} = params
1352 hide: Map.get(params, "irreversible", nil),
1353 whole_word: Map.get(params, "boolean", true)
1357 {:ok, response} = Filter.create(query)
1358 res = FilterView.render("filter.json", filter: response)
1362 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1363 filter = Filter.get(filter_id, user)
1364 res = FilterView.render("filter.json", filter: filter)
1369 %{assigns: %{user: user}} = conn,
1370 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1374 filter_id: filter_id,
1377 hide: Map.get(params, "irreversible", nil),
1378 whole_word: Map.get(params, "boolean", true)
1382 {:ok, response} = Filter.update(query)
1383 res = FilterView.render("filter.json", filter: response)
1387 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1390 filter_id: filter_id
1393 {:ok, _} = Filter.delete(query)
1399 def errors(conn, _) do
1402 |> json("Something went wrong")
1405 def suggestions(%{assigns: %{user: user}} = conn, _) do
1406 suggestions = Config.get(:suggestions)
1408 if Keyword.get(suggestions, :enabled, false) do
1409 api = Keyword.get(suggestions, :third_party_engine, "")
1410 timeout = Keyword.get(suggestions, :timeout, 5000)
1411 limit = Keyword.get(suggestions, :limit, 23)
1413 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1415 user = user.nickname
1419 |> String.replace("{{host}}", host)
1420 |> String.replace("{{user}}", user)
1422 with {:ok, %{status: 200, body: body}} <-
1427 recv_timeout: timeout,
1431 {:ok, data} <- Jason.decode(body) do
1434 |> Enum.slice(0, limit)
1439 case User.get_or_fetch(x["acct"]) do
1446 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1449 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1455 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1462 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1463 with %Activity{} = activity <- Repo.get(Activity, status_id),
1464 true <- Visibility.visible_for_user?(activity, user) do
1468 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1478 def reports(%{assigns: %{user: user}} = conn, params) do
1479 case CommonAPI.report(user, params) do
1482 |> put_view(ReportView)
1483 |> try_render("report.json", %{activity: activity})
1487 |> put_status(:bad_request)
1488 |> json(%{error: err})
1492 def try_render(conn, target, params)
1493 when is_binary(target) do
1494 res = render(conn, target, params)
1499 |> json(%{error: "Can't display this activity"})
1505 def try_render(conn, _, _) do
1508 |> json(%{error: "Can't display this activity"})
1511 defp present?(nil), do: false
1512 defp present?(false), do: false
1513 defp present?(_), do: true