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, followed, _} <- CommonAPI.follow(follower, followed) do
738 |> put_view(AccountView)
739 |> render("relationship.json", %{user: follower, target: followed})
743 |> put_resp_content_type("application/json")
744 |> send_resp(403, Jason.encode!(%{"error" => message}))
748 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
749 with %User{} = followed <- Repo.get_by(User, nickname: uri),
750 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
752 |> put_view(AccountView)
753 |> render("account.json", %{user: followed, for: follower})
757 |> put_resp_content_type("application/json")
758 |> send_resp(403, Jason.encode!(%{"error" => message}))
762 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
763 with %User{} = followed <- Repo.get(User, id),
764 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
765 {:ok, follower, _} <- User.unfollow(follower, followed) do
767 |> put_view(AccountView)
768 |> render("relationship.json", %{user: follower, target: followed})
772 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
773 with %User{} = muted <- Repo.get(User, id),
774 {:ok, muter} <- User.mute(muter, muted) do
776 |> put_view(AccountView)
777 |> render("relationship.json", %{user: muter, target: muted})
781 |> put_resp_content_type("application/json")
782 |> send_resp(403, Jason.encode!(%{"error" => message}))
786 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
787 with %User{} = muted <- Repo.get(User, id),
788 {:ok, muter} <- User.unmute(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 mutes(%{assigns: %{user: user}} = conn, _) do
801 with muted_accounts <- User.muted_users(user) do
802 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
807 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
808 with %User{} = blocked <- Repo.get(User, id),
809 {:ok, blocker} <- User.block(blocker, blocked),
810 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
812 |> put_view(AccountView)
813 |> render("relationship.json", %{user: blocker, target: blocked})
817 |> put_resp_content_type("application/json")
818 |> send_resp(403, Jason.encode!(%{"error" => message}))
822 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
823 with %User{} = blocked <- Repo.get(User, id),
824 {:ok, blocker} <- User.unblock(blocker, blocked),
825 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: blocker, target: blocked})
832 |> put_resp_content_type("application/json")
833 |> send_resp(403, Jason.encode!(%{"error" => message}))
837 def blocks(%{assigns: %{user: user}} = conn, _) do
838 with blocked_accounts <- User.blocked_users(user) do
839 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
844 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
845 json(conn, info.domain_blocks || [])
848 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
849 User.block_domain(blocker, domain)
853 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
854 User.unblock_domain(blocker, domain)
858 def status_search(user, query) do
860 if Regex.match?(~r/https?:/, query) do
861 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
862 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
863 true <- Visibility.visible_for_user?(activity, user) do
873 where: fragment("?->>'type' = 'Create'", a.data),
874 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
877 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
882 order_by: [desc: :id]
885 Repo.all(q) ++ fetched
888 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
889 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
891 statuses = status_search(user, query)
893 tags_path = Web.base_url() <> "/tag/"
899 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
900 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
901 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
904 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
906 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
913 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
914 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
916 statuses = status_search(user, query)
922 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
923 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
926 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
928 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
935 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
936 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
938 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
943 def favourites(%{assigns: %{user: user}} = conn, params) do
946 |> Map.put("type", "Create")
947 |> Map.put("favorited_by", user.ap_id)
948 |> Map.put("blocking_user", user)
949 |> ActivityPub.fetch_public_activities()
953 |> add_link_headers(:favourites, activities)
954 |> put_view(StatusView)
955 |> render("index.json", %{activities: activities, for: user, as: :activity})
958 def bookmarks(%{assigns: %{user: user}} = conn, _) do
959 user = Repo.get(User, user.id)
963 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
967 |> put_view(StatusView)
968 |> render("index.json", %{activities: activities, for: user, as: :activity})
971 def get_lists(%{assigns: %{user: user}} = conn, opts) do
972 lists = Pleroma.List.for_user(user, opts)
973 res = ListView.render("lists.json", lists: lists)
977 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
978 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
979 res = ListView.render("list.json", list: list)
985 |> json(%{error: "Record not found"})
989 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
990 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
991 res = ListView.render("lists.json", lists: lists)
995 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
996 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
997 {:ok, _list} <- Pleroma.List.delete(list) do
1005 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1006 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1007 res = ListView.render("list.json", list: list)
1012 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1014 |> Enum.each(fn account_id ->
1015 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1016 %User{} = followed <- Repo.get(User, account_id) do
1017 Pleroma.List.follow(list, followed)
1024 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1026 |> Enum.each(fn account_id ->
1027 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1028 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1029 Pleroma.List.unfollow(list, followed)
1036 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1037 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1038 {:ok, users} = Pleroma.List.get_following(list) do
1040 |> put_view(AccountView)
1041 |> render("accounts.json", %{users: users, as: :user})
1045 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1046 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1047 {:ok, list} <- Pleroma.List.rename(list, title) do
1048 res = ListView.render("list.json", list: list)
1056 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1057 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1060 |> Map.put("type", "Create")
1061 |> Map.put("blocking_user", user)
1062 |> Map.put("muting_user", user)
1064 # we must filter the following list for the user to avoid leaking statuses the user
1065 # does not actually have permission to see (for more info, peruse security issue #270).
1068 |> Enum.filter(fn x -> x in user.following end)
1069 |> ActivityPub.fetch_activities_bounded(following, params)
1073 |> put_view(StatusView)
1074 |> render("index.json", %{activities: activities, for: user, as: :activity})
1079 |> json(%{error: "Error."})
1083 def index(%{assigns: %{user: user}} = conn, _params) do
1086 |> get_session(:oauth_token)
1089 mastodon_emoji = mastodonized_emoji()
1091 limit = Config.get([:instance, :limit])
1094 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1096 flavour = get_user_flavour(user)
1101 streaming_api_base_url:
1102 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1103 access_token: token,
1105 domain: Pleroma.Web.Endpoint.host(),
1108 unfollow_modal: false,
1111 auto_play_gif: false,
1112 display_sensitive_media: false,
1113 reduce_motion: false,
1114 max_toot_chars: limit
1117 delete_others_notice: present?(user.info.is_moderator),
1118 admin: present?(user.info.is_admin)
1122 default_privacy: user.info.default_scope,
1123 default_sensitive: false
1125 media_attachments: %{
1126 accept_content_types: [
1142 user.info.settings ||
1172 push_subscription: nil,
1174 custom_emojis: mastodon_emoji,
1180 |> put_layout(false)
1181 |> put_view(MastodonView)
1182 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1185 |> redirect(to: "/web/login")
1189 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1190 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1192 with changeset <- Ecto.Changeset.change(user),
1193 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1194 {:ok, _user} <- User.update_and_set_cache(changeset) do
1199 |> put_resp_content_type("application/json")
1200 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1204 @supported_flavours ["glitch", "vanilla"]
1206 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1207 when flavour in @supported_flavours do
1208 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1210 with changeset <- Ecto.Changeset.change(user),
1211 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1212 {:ok, user} <- User.update_and_set_cache(changeset),
1213 flavour <- user.info.flavour do
1218 |> put_resp_content_type("application/json")
1219 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1223 def set_flavour(conn, _params) do
1226 |> json(%{error: "Unsupported flavour"})
1229 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1230 json(conn, get_user_flavour(user))
1233 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1237 defp get_user_flavour(_) do
1241 def login(conn, %{"code" => code}) do
1242 with {:ok, app} <- get_or_make_app(),
1243 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1244 {:ok, token} <- Token.exchange_token(app, auth) do
1246 |> put_session(:oauth_token, token.token)
1247 |> redirect(to: "/web/getting-started")
1251 def login(conn, _) do
1252 with {:ok, app} <- get_or_make_app() do
1257 response_type: "code",
1258 client_id: app.client_id,
1260 scope: Enum.join(app.scopes, " ")
1264 |> redirect(to: path)
1268 defp get_or_make_app() do
1269 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1270 scopes = ["read", "write", "follow", "push"]
1272 with %App{} = app <- Repo.get_by(App, find_attrs) do
1274 if app.scopes == scopes do
1278 |> Ecto.Changeset.change(%{scopes: scopes})
1286 App.register_changeset(
1288 Map.put(find_attrs, :scopes, scopes)
1295 def logout(conn, _) do
1298 |> redirect(to: "/")
1301 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1302 Logger.debug("Unimplemented, returning unmodified relationship")
1304 with %User{} = target <- Repo.get(User, id) do
1306 |> put_view(AccountView)
1307 |> render("relationship.json", %{user: user, target: target})
1311 def empty_array(conn, _) do
1312 Logger.debug("Unimplemented, returning an empty array")
1316 def empty_object(conn, _) do
1317 Logger.debug("Unimplemented, returning an empty object")
1321 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1322 actor = User.get_cached_by_ap_id(activity.data["actor"])
1323 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1324 mastodon_type = Activity.mastodon_notification_type(activity)
1328 type: mastodon_type,
1329 created_at: CommonAPI.Utils.to_masto_date(created_at),
1330 account: AccountView.render("account.json", %{user: actor, for: user})
1333 case mastodon_type do
1337 status: StatusView.render("status.json", %{activity: activity, for: user})
1343 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1349 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1360 def get_filters(%{assigns: %{user: user}} = conn, _) do
1361 filters = Filter.get_filters(user)
1362 res = FilterView.render("filters.json", filters: filters)
1367 %{assigns: %{user: user}} = conn,
1368 %{"phrase" => phrase, "context" => context} = params
1374 hide: Map.get(params, "irreversible", nil),
1375 whole_word: Map.get(params, "boolean", true)
1379 {:ok, response} = Filter.create(query)
1380 res = FilterView.render("filter.json", filter: response)
1384 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1385 filter = Filter.get(filter_id, user)
1386 res = FilterView.render("filter.json", filter: filter)
1391 %{assigns: %{user: user}} = conn,
1392 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1396 filter_id: filter_id,
1399 hide: Map.get(params, "irreversible", nil),
1400 whole_word: Map.get(params, "boolean", true)
1404 {:ok, response} = Filter.update(query)
1405 res = FilterView.render("filter.json", filter: response)
1409 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1412 filter_id: filter_id
1415 {:ok, _} = Filter.delete(query)
1419 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1420 true = Push.enabled()
1421 Subscription.delete_if_exists(user, token)
1422 {:ok, subscription} = Subscription.create(user, token, params)
1423 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1427 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1428 true = Push.enabled()
1429 subscription = Subscription.get(user, token)
1430 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1434 def update_push_subscription(
1435 %{assigns: %{user: user, token: token}} = conn,
1438 true = Push.enabled()
1439 {:ok, subscription} = Subscription.update(user, token, params)
1440 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1444 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1445 true = Push.enabled()
1446 {:ok, _response} = Subscription.delete(user, token)
1450 def errors(conn, _) do
1453 |> json("Something went wrong")
1456 def suggestions(%{assigns: %{user: user}} = conn, _) do
1457 suggestions = Config.get(:suggestions)
1459 if Keyword.get(suggestions, :enabled, false) do
1460 api = Keyword.get(suggestions, :third_party_engine, "")
1461 timeout = Keyword.get(suggestions, :timeout, 5000)
1462 limit = Keyword.get(suggestions, :limit, 23)
1464 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1466 user = user.nickname
1470 |> String.replace("{{host}}", host)
1471 |> String.replace("{{user}}", user)
1473 with {:ok, %{status: 200, body: body}} <-
1479 recv_timeout: timeout,
1483 {:ok, data} <- Jason.decode(body) do
1486 |> Enum.slice(0, limit)
1491 case User.get_or_fetch(x["acct"]) do
1498 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1501 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1507 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1514 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1515 with %Activity{} = activity <- Repo.get(Activity, status_id),
1516 true <- Visibility.visible_for_user?(activity, user) do
1520 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1530 def reports(%{assigns: %{user: user}} = conn, params) do
1531 case CommonAPI.report(user, params) do
1534 |> put_view(ReportView)
1535 |> try_render("report.json", %{activity: activity})
1539 |> put_status(:bad_request)
1540 |> json(%{error: err})
1544 def try_render(conn, target, params)
1545 when is_binary(target) do
1546 res = render(conn, target, params)
1551 |> json(%{error: "Can't display this activity"})
1557 def try_render(conn, _, _) do
1560 |> json(%{error: "Can't display this activity"})
1563 defp present?(nil), do: false
1564 defp present?(false), do: false
1565 defp present?(_), do: true