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
297 |> Map.put("type", "Create")
298 |> Map.put("blocking_user", user)
299 |> Map.put("user", user)
300 |> Map.put(:visibility, "direct")
303 ActivityPub.fetch_activities_query([user.ap_id], params)
307 |> add_link_headers(:dm_timeline, activities)
308 |> put_view(StatusView)
309 |> render("index.json", %{activities: activities, for: user, as: :activity})
312 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
313 with %Activity{} = activity <- Repo.get(Activity, id),
314 true <- Visibility.visible_for_user?(activity, user) do
316 |> put_view(StatusView)
317 |> try_render("status.json", %{activity: activity, for: user})
321 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
322 with %Activity{} = activity <- Repo.get(Activity, id),
324 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
325 "blocking_user" => user,
329 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
331 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
332 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
338 activities: grouped_activities[true] || [],
342 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
347 activities: grouped_activities[false] || [],
351 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
358 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
359 when length(media_ids) > 0 do
362 |> Map.put("status", ".")
364 post_status(conn, params)
367 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
370 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
373 case get_req_header(conn, "idempotency-key") do
375 _ -> Ecto.UUID.generate()
379 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
382 |> put_view(StatusView)
383 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
386 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
387 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
393 |> json(%{error: "Can't delete this post"})
397 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
398 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
400 |> put_view(StatusView)
401 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
405 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
406 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
407 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
409 |> put_view(StatusView)
410 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
414 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
415 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
416 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
418 |> put_view(StatusView)
419 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
423 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
424 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
425 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
427 |> put_view(StatusView)
428 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
432 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
433 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
435 |> put_view(StatusView)
436 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
440 |> put_resp_content_type("application/json")
441 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
445 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
446 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
453 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with %Activity{} = activity <- Repo.get(Activity, id),
455 %User{} = user <- User.get_by_nickname(user.nickname),
456 true <- Visibility.visible_for_user?(activity, user),
457 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
459 |> put_view(StatusView)
460 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
464 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
465 with %Activity{} = activity <- Repo.get(Activity, id),
466 %User{} = user <- User.get_by_nickname(user.nickname),
467 true <- Visibility.visible_for_user?(activity, user),
468 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
470 |> put_view(StatusView)
471 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
475 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
476 activity = Activity.get_by_id(id)
478 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
480 |> put_view(StatusView)
481 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
485 |> put_resp_content_type("application/json")
486 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
490 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
491 activity = Activity.get_by_id(id)
493 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
495 |> put_view(StatusView)
496 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
500 def notifications(%{assigns: %{user: user}} = conn, params) do
501 notifications = Notification.for_user(user, params)
505 |> Enum.map(fn x -> render_notification(user, x) end)
509 |> add_link_headers(:notifications, notifications)
513 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
514 with {:ok, notification} <- Notification.get(user, id) do
515 json(conn, render_notification(user, notification))
519 |> put_resp_content_type("application/json")
520 |> send_resp(403, Jason.encode!(%{"error" => reason}))
524 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
525 Notification.clear(user)
529 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
530 with {:ok, _notif} <- Notification.dismiss(user, id) do
535 |> put_resp_content_type("application/json")
536 |> send_resp(403, Jason.encode!(%{"error" => reason}))
540 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
542 q = from(u in User, where: u.id in ^id)
543 targets = Repo.all(q)
546 |> put_view(AccountView)
547 |> render("relationships.json", %{user: user, targets: targets})
550 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
551 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
553 def update_media(%{assigns: %{user: user}} = conn, data) do
554 with %Object{} = object <- Repo.get(Object, data["id"]),
555 true <- Object.authorize_mutation(object, user),
556 true <- is_binary(data["description"]),
557 description <- data["description"] do
558 new_data = %{object.data | "name" => description}
562 |> Object.change(%{data: new_data})
565 attachment_data = Map.put(new_data, "id", object.id)
568 |> put_view(StatusView)
569 |> render("attachment.json", %{attachment: attachment_data})
573 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
574 with {:ok, object} <-
577 actor: User.ap_id(user),
578 description: Map.get(data, "description")
580 attachment_data = Map.put(object.data, "id", object.id)
583 |> put_view(StatusView)
584 |> render("attachment.json", %{attachment: attachment_data})
588 def favourited_by(conn, %{"id" => id}) do
589 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
590 q = from(u in User, where: u.ap_id in ^likes)
594 |> put_view(AccountView)
595 |> render(AccountView, "accounts.json", %{users: users, as: :user})
601 def reblogged_by(conn, %{"id" => id}) do
602 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
603 q = from(u in User, where: u.ap_id in ^announces)
607 |> put_view(AccountView)
608 |> render("accounts.json", %{users: users, as: :user})
614 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
615 local_only = params["local"] in [true, "True", "true", "1"]
618 [params["tag"], params["any"]]
622 |> Enum.map(&String.downcase(&1))
627 |> Enum.map(&String.downcase(&1))
632 |> Enum.map(&String.downcase(&1))
636 |> Map.put("type", "Create")
637 |> Map.put("local_only", local_only)
638 |> Map.put("blocking_user", user)
639 |> Map.put("muting_user", user)
640 |> Map.put("tag", tags)
641 |> Map.put("tag_all", tag_all)
642 |> Map.put("tag_reject", tag_reject)
643 |> ActivityPub.fetch_public_activities()
647 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
648 |> put_view(StatusView)
649 |> render("index.json", %{activities: activities, for: user, as: :activity})
652 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
653 with %User{} = user <- Repo.get(User, id),
654 {:ok, followers} <- User.get_followers(user) do
657 for_user && user.id == for_user.id -> followers
658 user.info.hide_followers -> []
663 |> put_view(AccountView)
664 |> render("accounts.json", %{users: followers, as: :user})
668 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
669 with %User{} = user <- Repo.get(User, id),
670 {:ok, followers} <- User.get_friends(user) do
673 for_user && user.id == for_user.id -> followers
674 user.info.hide_follows -> []
679 |> put_view(AccountView)
680 |> render("accounts.json", %{users: followers, as: :user})
684 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
685 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
687 |> put_view(AccountView)
688 |> render("accounts.json", %{users: follow_requests, as: :user})
692 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
693 with %User{} = follower <- Repo.get(User, id),
694 {:ok, follower} <- User.maybe_follow(follower, followed),
695 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
696 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
698 ActivityPub.accept(%{
699 to: [follower.ap_id],
701 object: follow_activity.data["id"],
705 |> put_view(AccountView)
706 |> render("relationship.json", %{user: followed, target: follower})
710 |> put_resp_content_type("application/json")
711 |> send_resp(403, Jason.encode!(%{"error" => message}))
715 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
716 with %User{} = follower <- Repo.get(User, id),
717 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
718 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
720 ActivityPub.reject(%{
721 to: [follower.ap_id],
723 object: follow_activity.data["id"],
727 |> put_view(AccountView)
728 |> render("relationship.json", %{user: followed, target: follower})
732 |> put_resp_content_type("application/json")
733 |> send_resp(403, Jason.encode!(%{"error" => message}))
737 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
738 with %User{} = followed <- Repo.get(User, id),
739 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
741 |> put_view(AccountView)
742 |> render("relationship.json", %{user: follower, target: followed})
746 |> put_resp_content_type("application/json")
747 |> send_resp(403, Jason.encode!(%{"error" => message}))
751 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
752 with %User{} = followed <- Repo.get_by(User, nickname: uri),
753 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
755 |> put_view(AccountView)
756 |> render("account.json", %{user: followed, for: follower})
760 |> put_resp_content_type("application/json")
761 |> send_resp(403, Jason.encode!(%{"error" => message}))
765 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
766 with %User{} = followed <- Repo.get(User, id),
767 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
768 {:ok, follower, _} <- User.unfollow(follower, followed) do
770 |> put_view(AccountView)
771 |> render("relationship.json", %{user: follower, target: followed})
775 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
776 with %User{} = muted <- Repo.get(User, id),
777 {:ok, muter} <- User.mute(muter, muted) do
779 |> put_view(AccountView)
780 |> render("relationship.json", %{user: muter, target: muted})
784 |> put_resp_content_type("application/json")
785 |> send_resp(403, Jason.encode!(%{"error" => message}))
789 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
790 with %User{} = muted <- Repo.get(User, id),
791 {:ok, muter} <- User.unmute(muter, muted) do
793 |> put_view(AccountView)
794 |> render("relationship.json", %{user: muter, target: muted})
798 |> put_resp_content_type("application/json")
799 |> send_resp(403, Jason.encode!(%{"error" => message}))
803 def mutes(%{assigns: %{user: user}} = conn, _) do
804 with muted_accounts <- User.muted_users(user) do
805 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
810 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
811 with %User{} = blocked <- Repo.get(User, id),
812 {:ok, blocker} <- User.block(blocker, blocked),
813 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
815 |> put_view(AccountView)
816 |> render("relationship.json", %{user: blocker, target: blocked})
820 |> put_resp_content_type("application/json")
821 |> send_resp(403, Jason.encode!(%{"error" => message}))
825 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
826 with %User{} = blocked <- Repo.get(User, id),
827 {:ok, blocker} <- User.unblock(blocker, blocked),
828 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
830 |> put_view(AccountView)
831 |> render("relationship.json", %{user: blocker, target: blocked})
835 |> put_resp_content_type("application/json")
836 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 def blocks(%{assigns: %{user: user}} = conn, _) do
841 with blocked_accounts <- User.blocked_users(user) do
842 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
847 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
848 json(conn, info.domain_blocks || [])
851 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
852 User.block_domain(blocker, domain)
856 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
857 User.unblock_domain(blocker, domain)
861 def status_search(user, query) do
863 if Regex.match?(~r/https?:/, query) do
864 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
865 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
866 true <- Visibility.visible_for_user?(activity, user) do
876 where: fragment("?->>'type' = 'Create'", a.data),
877 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
880 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
885 order_by: [desc: :id]
888 Repo.all(q) ++ fetched
891 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
892 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
894 statuses = status_search(user, query)
896 tags_path = Web.base_url() <> "/tag/"
902 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
903 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
904 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
907 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
909 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
916 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
917 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
919 statuses = status_search(user, query)
925 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
926 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
929 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
931 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
938 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
939 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
941 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
946 def favourites(%{assigns: %{user: user}} = conn, params) do
949 |> Map.put("type", "Create")
950 |> Map.put("favorited_by", user.ap_id)
951 |> Map.put("blocking_user", user)
952 |> ActivityPub.fetch_public_activities()
956 |> add_link_headers(:favourites, activities)
957 |> put_view(StatusView)
958 |> render("index.json", %{activities: activities, for: user, as: :activity})
961 def bookmarks(%{assigns: %{user: user}} = conn, _) do
962 user = Repo.get(User, user.id)
966 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
970 |> put_view(StatusView)
971 |> render("index.json", %{activities: activities, for: user, as: :activity})
974 def get_lists(%{assigns: %{user: user}} = conn, opts) do
975 lists = Pleroma.List.for_user(user, opts)
976 res = ListView.render("lists.json", lists: lists)
980 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
981 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
982 res = ListView.render("list.json", list: list)
988 |> json(%{error: "Record not found"})
992 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
993 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
994 res = ListView.render("lists.json", lists: lists)
998 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
999 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1000 {:ok, _list} <- Pleroma.List.delete(list) do
1008 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1009 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1010 res = ListView.render("list.json", list: list)
1015 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1017 |> Enum.each(fn account_id ->
1018 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1019 %User{} = followed <- Repo.get(User, account_id) do
1020 Pleroma.List.follow(list, followed)
1027 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1029 |> Enum.each(fn account_id ->
1030 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1031 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1032 Pleroma.List.unfollow(list, followed)
1039 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1040 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1041 {:ok, users} = Pleroma.List.get_following(list) do
1043 |> put_view(AccountView)
1044 |> render("accounts.json", %{users: users, as: :user})
1048 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1049 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1050 {:ok, list} <- Pleroma.List.rename(list, title) do
1051 res = ListView.render("list.json", list: list)
1059 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1060 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1063 |> Map.put("type", "Create")
1064 |> Map.put("blocking_user", user)
1065 |> Map.put("muting_user", user)
1067 # we must filter the following list for the user to avoid leaking statuses the user
1068 # does not actually have permission to see (for more info, peruse security issue #270).
1071 |> Enum.filter(fn x -> x in user.following end)
1072 |> ActivityPub.fetch_activities_bounded(following, params)
1076 |> put_view(StatusView)
1077 |> render("index.json", %{activities: activities, for: user, as: :activity})
1082 |> json(%{error: "Error."})
1086 def index(%{assigns: %{user: user}} = conn, _params) do
1089 |> get_session(:oauth_token)
1092 mastodon_emoji = mastodonized_emoji()
1094 limit = Config.get([:instance, :limit])
1097 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1099 flavour = get_user_flavour(user)
1104 streaming_api_base_url:
1105 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1106 access_token: token,
1108 domain: Pleroma.Web.Endpoint.host(),
1111 unfollow_modal: false,
1114 auto_play_gif: false,
1115 display_sensitive_media: false,
1116 reduce_motion: false,
1117 max_toot_chars: limit
1120 delete_others_notice: present?(user.info.is_moderator),
1121 admin: present?(user.info.is_admin)
1125 default_privacy: user.info.default_scope,
1126 default_sensitive: false
1128 media_attachments: %{
1129 accept_content_types: [
1145 user.info.settings ||
1175 push_subscription: nil,
1177 custom_emojis: mastodon_emoji,
1183 |> put_layout(false)
1184 |> put_view(MastodonView)
1185 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1188 |> redirect(to: "/web/login")
1192 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1193 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1195 with changeset <- Ecto.Changeset.change(user),
1196 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1197 {:ok, _user} <- User.update_and_set_cache(changeset) do
1202 |> put_resp_content_type("application/json")
1203 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1207 @supported_flavours ["glitch", "vanilla"]
1209 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1210 when flavour in @supported_flavours do
1211 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1213 with changeset <- Ecto.Changeset.change(user),
1214 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1215 {:ok, user} <- User.update_and_set_cache(changeset),
1216 flavour <- user.info.flavour do
1221 |> put_resp_content_type("application/json")
1222 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1226 def set_flavour(conn, _params) do
1229 |> json(%{error: "Unsupported flavour"})
1232 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1233 json(conn, get_user_flavour(user))
1236 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1240 defp get_user_flavour(_) do
1244 def login(conn, %{"code" => code}) do
1245 with {:ok, app} <- get_or_make_app(),
1246 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1247 {:ok, token} <- Token.exchange_token(app, auth) do
1249 |> put_session(:oauth_token, token.token)
1250 |> redirect(to: "/web/getting-started")
1254 def login(conn, _) do
1255 with {:ok, app} <- get_or_make_app() do
1260 response_type: "code",
1261 client_id: app.client_id,
1263 scope: Enum.join(app.scopes, " ")
1267 |> redirect(to: path)
1271 defp get_or_make_app() do
1272 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1273 scopes = ["read", "write", "follow", "push"]
1275 with %App{} = app <- Repo.get_by(App, find_attrs) do
1277 if app.scopes == scopes do
1281 |> Ecto.Changeset.change(%{scopes: scopes})
1289 App.register_changeset(
1291 Map.put(find_attrs, :scopes, scopes)
1298 def logout(conn, _) do
1301 |> redirect(to: "/")
1304 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1305 Logger.debug("Unimplemented, returning unmodified relationship")
1307 with %User{} = target <- Repo.get(User, id) do
1309 |> put_view(AccountView)
1310 |> render("relationship.json", %{user: user, target: target})
1314 def empty_array(conn, _) do
1315 Logger.debug("Unimplemented, returning an empty array")
1319 def empty_object(conn, _) do
1320 Logger.debug("Unimplemented, returning an empty object")
1324 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1325 actor = User.get_cached_by_ap_id(activity.data["actor"])
1326 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1327 mastodon_type = Activity.mastodon_notification_type(activity)
1331 type: mastodon_type,
1332 created_at: CommonAPI.Utils.to_masto_date(created_at),
1333 account: AccountView.render("account.json", %{user: actor, for: user})
1336 case mastodon_type do
1340 status: StatusView.render("status.json", %{activity: activity, for: user})
1346 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1352 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1363 def get_filters(%{assigns: %{user: user}} = conn, _) do
1364 filters = Filter.get_filters(user)
1365 res = FilterView.render("filters.json", filters: filters)
1370 %{assigns: %{user: user}} = conn,
1371 %{"phrase" => phrase, "context" => context} = params
1377 hide: Map.get(params, "irreversible", nil),
1378 whole_word: Map.get(params, "boolean", true)
1382 {:ok, response} = Filter.create(query)
1383 res = FilterView.render("filter.json", filter: response)
1387 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1388 filter = Filter.get(filter_id, user)
1389 res = FilterView.render("filter.json", filter: filter)
1394 %{assigns: %{user: user}} = conn,
1395 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1399 filter_id: filter_id,
1402 hide: Map.get(params, "irreversible", nil),
1403 whole_word: Map.get(params, "boolean", true)
1407 {:ok, response} = Filter.update(query)
1408 res = FilterView.render("filter.json", filter: response)
1412 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1415 filter_id: filter_id
1418 {:ok, _} = Filter.delete(query)
1422 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1423 true = Push.enabled()
1424 Subscription.delete_if_exists(user, token)
1425 {:ok, subscription} = Subscription.create(user, token, params)
1426 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1430 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1431 true = Push.enabled()
1432 subscription = Subscription.get(user, token)
1433 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1437 def update_push_subscription(
1438 %{assigns: %{user: user, token: token}} = conn,
1441 true = Push.enabled()
1442 {:ok, subscription} = Subscription.update(user, token, params)
1443 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1447 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1448 true = Push.enabled()
1449 {:ok, _response} = Subscription.delete(user, token)
1453 def errors(conn, _) do
1456 |> json("Something went wrong")
1459 def suggestions(%{assigns: %{user: user}} = conn, _) do
1460 suggestions = Config.get(:suggestions)
1462 if Keyword.get(suggestions, :enabled, false) do
1463 api = Keyword.get(suggestions, :third_party_engine, "")
1464 timeout = Keyword.get(suggestions, :timeout, 5000)
1465 limit = Keyword.get(suggestions, :limit, 23)
1467 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1469 user = user.nickname
1473 |> String.replace("{{host}}", host)
1474 |> String.replace("{{user}}", user)
1476 with {:ok, %{status: 200, body: body}} <-
1482 recv_timeout: timeout,
1486 {:ok, data} <- Jason.decode(body) do
1489 |> Enum.slice(0, limit)
1494 case User.get_or_fetch(x["acct"]) do
1501 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1504 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1510 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1517 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1518 with %Activity{} = activity <- Repo.get(Activity, status_id),
1519 true <- Visibility.visible_for_user?(activity, user) do
1523 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1533 def reports(%{assigns: %{user: user}} = conn, params) do
1534 case CommonAPI.report(user, params) do
1537 |> put_view(ReportView)
1538 |> try_render("report.json", %{activity: activity})
1542 |> put_status(:bad_request)
1543 |> json(%{error: err})
1547 def try_render(conn, target, params)
1548 when is_binary(target) do
1549 res = render(conn, target, params)
1554 |> json(%{error: "Can't display this activity"})
1560 def try_render(conn, _, _) do
1563 |> json(%{error: "Can't display this activity"})
1566 defp present?(nil), do: false
1567 defp present?(false), do: false
1568 defp present?(_), do: true