1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Notification
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonView
26 alias Pleroma.Web.MastodonAPI.ReportView
27 alias Pleroma.Web.MastodonAPI.StatusView
28 alias Pleroma.Web.MediaProxy
29 alias Pleroma.Web.OAuth.App
30 alias Pleroma.Web.OAuth.Authorization
31 alias Pleroma.Web.OAuth.Token
33 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
38 @httpoison Application.get_env(:pleroma, :httpoison)
39 @local_mastodon_name "Mastodon-Local"
41 action_fallback(:errors)
43 def create_app(conn, params) do
44 scopes = oauth_scopes(params, ["read"])
48 |> Map.drop(["scope", "scopes"])
49 |> Map.put("scopes", scopes)
51 with cs <- App.register_changeset(%App{}, app_attrs),
52 false <- cs.changes[:client_name] == @local_mastodon_name,
53 {:ok, app} <- Repo.insert(cs) do
55 id: app.id |> to_string,
56 name: app.client_name,
57 client_id: app.client_id,
58 client_secret: app.client_secret,
59 redirect_uri: app.redirect_uris,
72 value_function \\ fn x -> {:ok, x} end
74 if Map.has_key?(params, params_field) do
75 case value_function.(params[params_field]) do
76 {:ok, new_value} -> Map.put(map, map_field, new_value)
84 def update_credentials(%{assigns: %{user: user}} = conn, params) do
89 |> add_if_present(params, "display_name", :name)
90 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
91 |> add_if_present(params, "avatar", :avatar, fn value ->
92 with %Plug.Upload{} <- value,
93 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
102 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
103 |> add_if_present(params, "header", :banner, fn value ->
104 with %Plug.Upload{} <- value,
105 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
112 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
114 with changeset <- User.update_changeset(user, user_params),
115 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
116 {:ok, user} <- User.update_and_set_cache(changeset) do
117 if original_user != user do
118 CommonAPI.update(user)
121 json(conn, AccountView.render("account.json", %{user: user, for: user}))
126 |> json(%{error: "Invalid request"})
130 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131 account = AccountView.render("account.json", %{user: user, for: user})
135 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
136 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
137 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
138 account = AccountView.render("account.json", %{user: user, for: for_user})
144 |> json(%{error: "Can't find user"})
148 @mastodon_api_level "2.5.0"
150 def masto_instance(conn, _params) do
151 instance = Config.get(:instance)
155 title: Keyword.get(instance, :name),
156 description: Keyword.get(instance, :description),
157 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
158 email: Keyword.get(instance, :email),
160 streaming_api: Pleroma.Web.Endpoint.websocket_url()
162 stats: Stats.get_stats(),
163 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
164 max_toot_chars: Keyword.get(instance, :limit)
170 def peers(conn, _params) do
171 json(conn, Stats.get_peers())
174 defp mastodonized_emoji do
175 Pleroma.Emoji.get_all()
176 |> Enum.map(fn {shortcode, relative_url} ->
177 url = to_string(URI.merge(Web.base_url(), relative_url))
180 "shortcode" => shortcode,
182 "visible_in_picker" => true,
188 def custom_emojis(conn, _params) do
189 mastodon_emoji = mastodonized_emoji()
190 json(conn, mastodon_emoji)
193 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
196 |> Map.drop(["since_id", "max_id"])
199 last = List.last(activities)
200 first = List.first(activities)
206 {next_url, prev_url} =
210 Pleroma.Web.Endpoint,
213 Map.merge(params, %{max_id: min})
216 Pleroma.Web.Endpoint,
219 Map.merge(params, %{since_id: max})
225 Pleroma.Web.Endpoint,
227 Map.merge(params, %{max_id: min})
230 Pleroma.Web.Endpoint,
232 Map.merge(params, %{since_id: max})
238 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
244 def home_timeline(%{assigns: %{user: user}} = conn, params) do
247 |> Map.put("type", ["Create", "Announce"])
248 |> Map.put("blocking_user", user)
249 |> Map.put("muting_user", user)
250 |> Map.put("user", user)
253 [user.ap_id | user.following]
254 |> ActivityPub.fetch_activities(params)
255 |> ActivityPub.contain_timeline(user)
259 |> add_link_headers(:home_timeline, activities)
260 |> put_view(StatusView)
261 |> render("index.json", %{activities: activities, for: user, as: :activity})
264 def public_timeline(%{assigns: %{user: user}} = conn, params) do
265 local_only = params["local"] in [true, "True", "true", "1"]
269 |> Map.put("type", ["Create", "Announce"])
270 |> Map.put("local_only", local_only)
271 |> Map.put("blocking_user", user)
272 |> Map.put("muting_user", user)
273 |> ActivityPub.fetch_public_activities()
277 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
278 |> put_view(StatusView)
279 |> render("index.json", %{activities: activities, for: user, as: :activity})
282 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
283 with %User{} = user <- Repo.get(User, params["id"]) do
284 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
287 |> add_link_headers(:user_statuses, activities, params["id"])
288 |> put_view(StatusView)
289 |> render("index.json", %{
290 activities: activities,
297 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
300 |> Map.put("type", "Create")
301 |> Map.put("blocking_user", user)
302 |> Map.put("user", user)
303 |> Map.put(:visibility, "direct")
307 |> ActivityPub.fetch_activities_query(params)
311 |> add_link_headers(:dm_timeline, activities)
312 |> put_view(StatusView)
313 |> render("index.json", %{activities: activities, for: user, as: :activity})
316 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
317 with %Activity{} = activity <- Repo.get(Activity, id),
318 true <- Visibility.visible_for_user?(activity, user) do
320 |> put_view(StatusView)
321 |> try_render("status.json", %{activity: activity, for: user})
325 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
326 with %Activity{} = activity <- Repo.get(Activity, id),
328 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
329 "blocking_user" => user,
333 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
335 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
336 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
342 activities: grouped_activities[true] || [],
346 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
351 activities: grouped_activities[false] || [],
355 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
362 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
363 when length(media_ids) > 0 do
366 |> Map.put("status", ".")
368 post_status(conn, params)
371 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
374 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
377 case get_req_header(conn, "idempotency-key") do
379 _ -> Ecto.UUID.generate()
383 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
386 |> put_view(StatusView)
387 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
390 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
391 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
397 |> json(%{error: "Can't delete this post"})
401 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
402 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
404 |> put_view(StatusView)
405 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
409 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
411 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
413 |> put_view(StatusView)
414 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
418 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
419 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
420 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
422 |> put_view(StatusView)
423 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
427 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
428 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
429 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
431 |> put_view(StatusView)
432 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
436 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
437 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
439 |> put_view(StatusView)
440 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
444 |> put_resp_content_type("application/json")
445 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
449 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
450 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
452 |> put_view(StatusView)
453 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
457 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
458 with %Activity{} = activity <- Repo.get(Activity, id),
459 %User{} = user <- User.get_by_nickname(user.nickname),
460 true <- Visibility.visible_for_user?(activity, user),
461 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
463 |> put_view(StatusView)
464 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
468 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
469 with %Activity{} = activity <- Repo.get(Activity, id),
470 %User{} = user <- User.get_by_nickname(user.nickname),
471 true <- Visibility.visible_for_user?(activity, user),
472 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
474 |> put_view(StatusView)
475 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
479 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 activity = Activity.get_by_id(id)
482 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
484 |> put_view(StatusView)
485 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
489 |> put_resp_content_type("application/json")
490 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
494 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
495 activity = Activity.get_by_id(id)
497 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
499 |> put_view(StatusView)
500 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
504 def notifications(%{assigns: %{user: user}} = conn, params) do
505 notifications = Notification.for_user(user, params)
509 |> Enum.map(fn x -> render_notification(user, x) end)
513 |> add_link_headers(:notifications, notifications)
517 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
518 with {:ok, notification} <- Notification.get(user, id) do
519 json(conn, render_notification(user, notification))
523 |> put_resp_content_type("application/json")
524 |> send_resp(403, Jason.encode!(%{"error" => reason}))
528 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
529 Notification.clear(user)
533 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
534 with {:ok, _notif} <- Notification.dismiss(user, id) do
539 |> put_resp_content_type("application/json")
540 |> send_resp(403, Jason.encode!(%{"error" => reason}))
544 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
546 q = from(u in User, where: u.id in ^id)
547 targets = Repo.all(q)
550 |> put_view(AccountView)
551 |> render("relationships.json", %{user: user, targets: targets})
554 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
555 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
557 def update_media(%{assigns: %{user: user}} = conn, data) do
558 with %Object{} = object <- Repo.get(Object, data["id"]),
559 true <- Object.authorize_mutation(object, user),
560 true <- is_binary(data["description"]),
561 description <- data["description"] do
562 new_data = %{object.data | "name" => description}
566 |> Object.change(%{data: new_data})
569 attachment_data = Map.put(new_data, "id", object.id)
572 |> put_view(StatusView)
573 |> render("attachment.json", %{attachment: attachment_data})
577 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
578 with {:ok, object} <-
581 actor: User.ap_id(user),
582 description: Map.get(data, "description")
584 attachment_data = Map.put(object.data, "id", object.id)
587 |> put_view(StatusView)
588 |> render("attachment.json", %{attachment: attachment_data})
592 def favourited_by(conn, %{"id" => id}) do
593 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
594 q = from(u in User, where: u.ap_id in ^likes)
598 |> put_view(AccountView)
599 |> render(AccountView, "accounts.json", %{users: users, as: :user})
605 def reblogged_by(conn, %{"id" => id}) do
606 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
607 q = from(u in User, where: u.ap_id in ^announces)
611 |> put_view(AccountView)
612 |> render("accounts.json", %{users: users, as: :user})
618 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
619 local_only = params["local"] in [true, "True", "true", "1"]
622 [params["tag"], params["any"]]
626 |> Enum.map(&String.downcase(&1))
631 |> Enum.map(&String.downcase(&1))
636 |> Enum.map(&String.downcase(&1))
640 |> Map.put("type", "Create")
641 |> Map.put("local_only", local_only)
642 |> Map.put("blocking_user", user)
643 |> Map.put("muting_user", user)
644 |> Map.put("tag", tags)
645 |> Map.put("tag_all", tag_all)
646 |> Map.put("tag_reject", tag_reject)
647 |> ActivityPub.fetch_public_activities()
651 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
652 |> put_view(StatusView)
653 |> render("index.json", %{activities: activities, for: user, as: :activity})
656 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
657 with %User{} = user <- Repo.get(User, id),
658 followers <- MastodonAPI.get_followers(user, params) do
661 for_user && user.id == for_user.id -> followers
662 user.info.hide_followers -> []
667 |> add_link_headers(:followers, followers, user)
668 |> put_view(AccountView)
669 |> render("accounts.json", %{users: followers, as: :user})
673 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
674 with %User{} = user <- Repo.get(User, id),
675 followers <- MastodonAPI.get_friends(user, params) do
678 for_user && user.id == for_user.id -> followers
679 user.info.hide_follows -> []
684 |> add_link_headers(:following, followers, user)
685 |> put_view(AccountView)
686 |> render("accounts.json", %{users: followers, as: :user})
690 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
691 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
693 |> put_view(AccountView)
694 |> render("accounts.json", %{users: follow_requests, as: :user})
698 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
699 with %User{} = follower <- Repo.get(User, id),
700 {:ok, follower} <- User.maybe_follow(follower, followed),
701 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
702 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
704 ActivityPub.accept(%{
705 to: [follower.ap_id],
707 object: follow_activity.data["id"],
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 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
724 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
726 ActivityPub.reject(%{
727 to: [follower.ap_id],
729 object: follow_activity.data["id"],
733 |> put_view(AccountView)
734 |> render("relationship.json", %{user: followed, target: follower})
738 |> put_resp_content_type("application/json")
739 |> send_resp(403, Jason.encode!(%{"error" => message}))
743 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
744 with %User{} = followed <- Repo.get(User, id),
745 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
747 |> put_view(AccountView)
748 |> render("relationship.json", %{user: follower, target: followed})
752 |> put_resp_content_type("application/json")
753 |> send_resp(403, Jason.encode!(%{"error" => message}))
757 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
758 with %User{} = followed <- Repo.get_by(User, nickname: uri),
759 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
761 |> put_view(AccountView)
762 |> render("account.json", %{user: followed, for: follower})
766 |> put_resp_content_type("application/json")
767 |> send_resp(403, Jason.encode!(%{"error" => message}))
771 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
772 with %User{} = followed <- Repo.get(User, id),
773 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
774 {:ok, follower, _} <- User.unfollow(follower, followed) do
776 |> put_view(AccountView)
777 |> render("relationship.json", %{user: follower, target: followed})
781 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
782 with %User{} = muted <- Repo.get(User, id),
783 {:ok, muter} <- User.mute(muter, muted) do
785 |> put_view(AccountView)
786 |> render("relationship.json", %{user: muter, target: muted})
790 |> put_resp_content_type("application/json")
791 |> send_resp(403, Jason.encode!(%{"error" => message}))
795 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
796 with %User{} = muted <- Repo.get(User, id),
797 {:ok, muter} <- User.unmute(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 mutes(%{assigns: %{user: user}} = conn, _) do
810 with muted_accounts <- User.muted_users(user) do
811 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
816 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
817 with %User{} = blocked <- Repo.get(User, id),
818 {:ok, blocker} <- User.block(blocker, blocked),
819 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
821 |> put_view(AccountView)
822 |> render("relationship.json", %{user: blocker, target: blocked})
826 |> put_resp_content_type("application/json")
827 |> send_resp(403, Jason.encode!(%{"error" => message}))
831 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
832 with %User{} = blocked <- Repo.get(User, id),
833 {:ok, blocker} <- User.unblock(blocker, blocked),
834 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
836 |> put_view(AccountView)
837 |> render("relationship.json", %{user: blocker, target: blocked})
841 |> put_resp_content_type("application/json")
842 |> send_resp(403, Jason.encode!(%{"error" => message}))
846 def blocks(%{assigns: %{user: user}} = conn, _) do
847 with blocked_accounts <- User.blocked_users(user) do
848 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
853 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
854 json(conn, info.domain_blocks || [])
857 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
858 User.block_domain(blocker, domain)
862 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
863 User.unblock_domain(blocker, domain)
867 def status_search(user, query) do
869 if Regex.match?(~r/https?:/, query) do
870 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
871 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
872 true <- Visibility.visible_for_user?(activity, user) do
882 where: fragment("?->>'type' = 'Create'", a.data),
883 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
886 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
891 order_by: [desc: :id]
894 Repo.all(q) ++ fetched
897 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
898 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
900 statuses = status_search(user, query)
902 tags_path = Web.base_url() <> "/tag/"
908 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
909 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
910 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
913 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
915 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
922 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
923 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
925 statuses = status_search(user, query)
931 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
932 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
935 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
937 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
944 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
945 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
947 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
952 def favourites(%{assigns: %{user: user}} = conn, params) do
955 |> Map.put("type", "Create")
956 |> Map.put("favorited_by", user.ap_id)
957 |> Map.put("blocking_user", user)
958 |> ActivityPub.fetch_public_activities()
962 |> add_link_headers(:favourites, activities)
963 |> put_view(StatusView)
964 |> render("index.json", %{activities: activities, for: user, as: :activity})
967 def bookmarks(%{assigns: %{user: user}} = conn, _) do
968 user = Repo.get(User, user.id)
972 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
976 |> put_view(StatusView)
977 |> render("index.json", %{activities: activities, for: user, as: :activity})
980 def get_lists(%{assigns: %{user: user}} = conn, opts) do
981 lists = Pleroma.List.for_user(user, opts)
982 res = ListView.render("lists.json", lists: lists)
986 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
987 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
988 res = ListView.render("list.json", list: list)
994 |> json(%{error: "Record not found"})
998 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
999 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1000 res = ListView.render("lists.json", lists: lists)
1004 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1005 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1006 {:ok, _list} <- Pleroma.List.delete(list) do
1014 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1015 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1016 res = ListView.render("list.json", list: list)
1021 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1023 |> Enum.each(fn account_id ->
1024 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1025 %User{} = followed <- Repo.get(User, account_id) do
1026 Pleroma.List.follow(list, followed)
1033 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1035 |> Enum.each(fn account_id ->
1036 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1037 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1038 Pleroma.List.unfollow(list, followed)
1045 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1046 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1047 {:ok, users} = Pleroma.List.get_following(list) do
1049 |> put_view(AccountView)
1050 |> render("accounts.json", %{users: users, as: :user})
1054 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1055 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1056 {:ok, list} <- Pleroma.List.rename(list, title) do
1057 res = ListView.render("list.json", list: list)
1065 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1066 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1069 |> Map.put("type", "Create")
1070 |> Map.put("blocking_user", user)
1071 |> Map.put("muting_user", user)
1073 # we must filter the following list for the user to avoid leaking statuses the user
1074 # does not actually have permission to see (for more info, peruse security issue #270).
1077 |> Enum.filter(fn x -> x in user.following end)
1078 |> ActivityPub.fetch_activities_bounded(following, params)
1082 |> put_view(StatusView)
1083 |> render("index.json", %{activities: activities, for: user, as: :activity})
1088 |> json(%{error: "Error."})
1092 def index(%{assigns: %{user: user}} = conn, _params) do
1095 |> get_session(:oauth_token)
1098 mastodon_emoji = mastodonized_emoji()
1100 limit = Config.get([:instance, :limit])
1103 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1105 flavour = get_user_flavour(user)
1110 streaming_api_base_url:
1111 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1112 access_token: token,
1114 domain: Pleroma.Web.Endpoint.host(),
1117 unfollow_modal: false,
1120 auto_play_gif: false,
1121 display_sensitive_media: false,
1122 reduce_motion: false,
1123 max_toot_chars: limit
1126 delete_others_notice: present?(user.info.is_moderator),
1127 admin: present?(user.info.is_admin)
1131 default_privacy: user.info.default_scope,
1132 default_sensitive: false
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)
1430 def errors(conn, _) do
1433 |> json("Something went wrong")
1436 def suggestions(%{assigns: %{user: user}} = conn, _) do
1437 suggestions = Config.get(:suggestions)
1439 if Keyword.get(suggestions, :enabled, false) do
1440 api = Keyword.get(suggestions, :third_party_engine, "")
1441 timeout = Keyword.get(suggestions, :timeout, 5000)
1442 limit = Keyword.get(suggestions, :limit, 23)
1444 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1446 user = user.nickname
1450 |> String.replace("{{host}}", host)
1451 |> String.replace("{{user}}", user)
1453 with {:ok, %{status: 200, body: body}} <-
1458 recv_timeout: timeout,
1462 {:ok, data} <- Jason.decode(body) do
1465 |> Enum.slice(0, limit)
1470 case User.get_or_fetch(x["acct"]) do
1477 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1480 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1486 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1493 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1494 with %Activity{} = activity <- Repo.get(Activity, status_id),
1495 true <- Visibility.visible_for_user?(activity, user) do
1499 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1509 def reports(%{assigns: %{user: user}} = conn, params) do
1510 case CommonAPI.report(user, params) do
1513 |> put_view(ReportView)
1514 |> try_render("report.json", %{activity: activity})
1518 |> put_status(:bad_request)
1519 |> json(%{error: err})
1523 def try_render(conn, target, params)
1524 when is_binary(target) do
1525 res = render(conn, target, params)
1530 |> json(%{error: "Can't display this activity"})
1536 def try_render(conn, _, _) do
1539 |> json(%{error: "Can't display this activity"})
1542 defp present?(nil), do: false
1543 defp present?(false), do: false
1544 defp present?(_), do: true