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