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
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
23 alias Pleroma.ScheduledActivity
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Visibility
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.MastodonAPI.AccountView
31 alias Pleroma.Web.MastodonAPI.AppView
32 alias Pleroma.Web.MastodonAPI.ConversationView
33 alias Pleroma.Web.MastodonAPI.FilterView
34 alias Pleroma.Web.MastodonAPI.ListView
35 alias Pleroma.Web.MastodonAPI.MastodonAPI
36 alias Pleroma.Web.MastodonAPI.MastodonView
37 alias Pleroma.Web.MastodonAPI.ReportView
38 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
39 alias Pleroma.Web.MastodonAPI.StatusView
40 alias Pleroma.Web.MediaProxy
41 alias Pleroma.Web.OAuth.App
42 alias Pleroma.Web.OAuth.Authorization
43 alias Pleroma.Web.OAuth.Scopes
44 alias Pleroma.Web.OAuth.Token
45 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 require Pleroma.Constants
52 @rate_limited_relations_actions ~w(follow unfollow)a
56 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
59 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
60 plug(RateLimiter, :app_account_creation when action == :account_register)
61 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
62 plug(RateLimiter, :password_reset when action == :password_reset)
63 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
65 @local_mastodon_name "Mastodon-Local"
67 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
69 def create_app(conn, params) do
70 scopes = Scopes.fetch_scopes(params, ["read"])
74 |> Map.drop(["scope", "scopes"])
75 |> Map.put("scopes", scopes)
77 with cs <- App.register_changeset(%App{}, app_attrs),
78 false <- cs.changes[:client_name] == @local_mastodon_name,
79 {:ok, app} <- Repo.insert(cs) do
82 |> render("show.json", %{app: app})
91 value_function \\ fn x -> {:ok, x} end
93 if Map.has_key?(params, params_field) do
94 case value_function.(params[params_field]) do
95 {:ok, new_value} -> Map.put(map, map_field, new_value)
103 def update_credentials(%{assigns: %{user: user}} = conn, params) do
108 |> add_if_present(params, "display_name", :name)
109 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
110 |> add_if_present(params, "avatar", :avatar, fn value ->
111 with %Plug.Upload{} <- value,
112 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
119 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
123 |> Map.get(:emoji, [])
124 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
131 :hide_followers_count,
137 :skip_thread_containment,
140 |> Enum.reduce(%{}, fn key, acc ->
141 add_if_present(acc, params, to_string(key), key, fn value ->
142 {:ok, truthy_param?(value)}
145 |> add_if_present(params, "default_scope", :default_scope)
146 |> add_if_present(params, "fields", :fields, fn fields ->
147 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
151 |> add_if_present(params, "fields", :raw_fields)
152 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
153 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
155 |> add_if_present(params, "header", :banner, fn value ->
156 with %Plug.Upload{} <- value,
157 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
163 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
164 with %Plug.Upload{} <- value,
165 {:ok, object} <- ActivityPub.upload(value, type: :background) do
171 |> Map.put(:emoji, user_info_emojis)
175 |> User.update_changeset(user_params)
176 |> User.change_info(&User.Info.profile_update(&1, info_params))
178 with {:ok, user} <- User.update_and_set_cache(changeset) do
179 if original_user != user, do: CommonAPI.update(user)
183 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
186 _e -> render_error(conn, :forbidden, "Invalid request")
190 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
191 change = Changeset.change(user, %{avatar: nil})
192 {:ok, user} = User.update_and_set_cache(change)
193 CommonAPI.update(user)
195 json(conn, %{url: nil})
198 def update_avatar(%{assigns: %{user: user}} = conn, params) do
199 {:ok, object} = ActivityPub.upload(params, type: :avatar)
200 change = Changeset.change(user, %{avatar: object.data})
201 {:ok, user} = User.update_and_set_cache(change)
202 CommonAPI.update(user)
203 %{"url" => [%{"href" => href} | _]} = object.data
205 json(conn, %{url: href})
208 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
209 new_info = %{"banner" => %{}}
211 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
212 CommonAPI.update(user)
213 json(conn, %{url: nil})
217 def update_banner(%{assigns: %{user: user}} = conn, params) do
218 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
219 new_info <- %{"banner" => object.data},
220 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
221 CommonAPI.update(user)
222 %{"url" => [%{"href" => href} | _]} = object.data
224 json(conn, %{url: href})
228 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
229 new_info = %{"background" => %{}}
231 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
232 json(conn, %{url: nil})
236 def update_background(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(params, type: :background),
238 new_info <- %{"background" => object.data},
239 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
240 %{"url" => [%{"href" => href} | _]} = object.data
242 json(conn, %{url: href})
246 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
247 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
250 AccountView.render("account.json", %{
253 with_pleroma_settings: true,
254 with_chat_token: chat_token
260 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
261 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
264 |> render("short.json", %{app: app})
268 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
269 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
270 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
271 account = AccountView.render("account.json", %{user: user, for: for_user})
274 _e -> render_error(conn, :not_found, "Can't find user")
278 @mastodon_api_level "2.7.2"
280 def masto_instance(conn, _params) do
281 instance = Config.get(:instance)
285 title: Keyword.get(instance, :name),
286 description: Keyword.get(instance, :description),
287 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
288 email: Keyword.get(instance, :email),
290 streaming_api: Pleroma.Web.Endpoint.websocket_url()
292 stats: Stats.get_stats(),
293 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
295 registrations: Pleroma.Config.get([:instance, :registrations_open]),
296 # Extra (not present in Mastodon):
297 max_toot_chars: Keyword.get(instance, :limit),
298 poll_limits: Keyword.get(instance, :poll_limits)
304 def peers(conn, _params) do
305 json(conn, Stats.get_peers())
308 defp mastodonized_emoji do
309 Pleroma.Emoji.get_all()
310 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
311 url = to_string(URI.merge(Web.base_url(), relative_url))
314 "shortcode" => shortcode,
316 "visible_in_picker" => true,
319 # Assuming that a comma is authorized in the category name
320 "category" => (tags -- ["Custom"]) |> Enum.join(",")
325 def custom_emojis(conn, _params) do
326 mastodon_emoji = mastodonized_emoji()
327 json(conn, mastodon_emoji)
330 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
331 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
334 |> Map.put("tag", params["tagged"])
336 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
339 |> add_link_headers(activities)
340 |> put_view(StatusView)
341 |> render("index.json", %{
342 activities: activities,
349 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
351 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
352 true <- Visibility.visible_for_user?(activity, user) do
354 |> put_view(StatusView)
355 |> try_render("poll.json", %{object: object, for: user})
357 error when is_nil(error) or error == false ->
358 render_error(conn, :not_found, "Record not found")
362 defp get_cached_vote_or_vote(user, object, choices) do
363 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
366 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
367 case CommonAPI.vote(user, object, choices) do
368 {:error, _message} = res -> {:ignore, res}
369 res -> {:commit, res}
376 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
377 with %Object{} = object <- Object.get_by_id(id),
378 true <- object.data["type"] == "Question",
379 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
380 true <- Visibility.visible_for_user?(activity, user),
381 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
383 |> put_view(StatusView)
384 |> try_render("poll.json", %{object: object, for: user})
387 render_error(conn, :not_found, "Record not found")
390 render_error(conn, :not_found, "Record not found")
394 |> put_status(:unprocessable_entity)
395 |> json(%{error: message})
399 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
400 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
402 |> add_link_headers(scheduled_activities)
403 |> put_view(ScheduledActivityView)
404 |> render("index.json", %{scheduled_activities: scheduled_activities})
408 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
409 with %ScheduledActivity{} = scheduled_activity <-
410 ScheduledActivity.get(user, scheduled_activity_id) do
412 |> put_view(ScheduledActivityView)
413 |> render("show.json", %{scheduled_activity: scheduled_activity})
415 _ -> {:error, :not_found}
419 def update_scheduled_status(
420 %{assigns: %{user: user}} = conn,
421 %{"id" => scheduled_activity_id} = params
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
436 with %ScheduledActivity{} = scheduled_activity <-
437 ScheduledActivity.get(user, scheduled_activity_id),
438 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
440 |> put_view(ScheduledActivityView)
441 |> render("show.json", %{scheduled_activity: scheduled_activity})
443 nil -> {:error, :not_found}
448 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
450 q = from(u in User, where: u.id in ^id)
451 targets = Repo.all(q)
454 |> put_view(AccountView)
455 |> render("relationships.json", %{user: user, targets: targets})
458 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
459 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
461 def update_media(%{assigns: %{user: user}} = conn, data) do
462 with %Object{} = object <- Repo.get(Object, data["id"]),
463 true <- Object.authorize_mutation(object, user),
464 true <- is_binary(data["description"]),
465 description <- data["description"] do
466 new_data = %{object.data | "name" => description}
470 |> Object.change(%{data: new_data})
473 attachment_data = Map.put(new_data, "id", object.id)
476 |> put_view(StatusView)
477 |> render("attachment.json", %{attachment: attachment_data})
481 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
482 with {:ok, object} <-
485 actor: User.ap_id(user),
486 description: Map.get(data, "description")
488 attachment_data = Map.put(object.data, "id", object.id)
491 |> put_view(StatusView)
492 |> render("attachment.json", %{attachment: attachment_data})
496 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
497 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
498 %{} = attachment_data <- Map.put(object.data, "id", object.id),
499 # Reject if not an image
500 %{type: "image"} = rendered <-
501 StatusView.render("attachment.json", %{attachment: attachment_data}) do
503 # Save to the user's info
504 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
508 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
512 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
513 mascot = User.get_mascot(user)
519 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
520 with %User{} = user <- User.get_cached_by_id(id),
521 followers <- MastodonAPI.get_followers(user, params) do
524 for_user && user.id == for_user.id -> followers
525 user.info.hide_followers -> []
530 |> add_link_headers(followers)
531 |> put_view(AccountView)
532 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
536 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
537 with %User{} = user <- User.get_cached_by_id(id),
538 followers <- MastodonAPI.get_friends(user, params) do
541 for_user && user.id == for_user.id -> followers
542 user.info.hide_follows -> []
547 |> add_link_headers(followers)
548 |> put_view(AccountView)
549 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
553 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
554 follow_requests = User.get_follow_requests(followed)
557 |> put_view(AccountView)
558 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
561 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
562 with %User{} = follower <- User.get_cached_by_id(id),
563 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
565 |> put_view(AccountView)
566 |> render("relationship.json", %{user: followed, target: follower})
570 |> put_status(:forbidden)
571 |> json(%{error: message})
575 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
576 with %User{} = follower <- User.get_cached_by_id(id),
577 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
579 |> put_view(AccountView)
580 |> render("relationship.json", %{user: followed, target: follower})
584 |> put_status(:forbidden)
585 |> json(%{error: message})
589 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
590 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
591 {_, true} <- {:followed, follower.id != followed.id},
592 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
594 |> put_view(AccountView)
595 |> render("relationship.json", %{user: follower, target: followed})
602 |> put_status(:forbidden)
603 |> json(%{error: message})
607 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
608 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
609 {_, true} <- {:followed, follower.id != followed.id},
610 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
612 |> put_view(AccountView)
613 |> render("account.json", %{user: followed, for: follower})
620 |> put_status(:forbidden)
621 |> json(%{error: message})
625 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
626 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
627 {_, true} <- {:followed, follower.id != followed.id},
628 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
630 |> put_view(AccountView)
631 |> render("relationship.json", %{user: follower, target: followed})
641 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
643 if Map.has_key?(params, "notifications"),
644 do: params["notifications"] in [true, "True", "true", "1"],
647 with %User{} = muted <- User.get_cached_by_id(id),
648 {:ok, muter} <- User.mute(muter, muted, notifications) do
650 |> put_view(AccountView)
651 |> render("relationship.json", %{user: muter, target: muted})
655 |> put_status(:forbidden)
656 |> json(%{error: message})
660 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
661 with %User{} = muted <- User.get_cached_by_id(id),
662 {:ok, muter} <- User.unmute(muter, muted) do
664 |> put_view(AccountView)
665 |> render("relationship.json", %{user: muter, target: muted})
669 |> put_status(:forbidden)
670 |> json(%{error: message})
674 def mutes(%{assigns: %{user: user}} = conn, _) do
675 with muted_accounts <- User.muted_users(user) do
676 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
681 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
682 with %User{} = blocked <- User.get_cached_by_id(id),
683 {:ok, blocker} <- User.block(blocker, blocked),
684 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
686 |> put_view(AccountView)
687 |> render("relationship.json", %{user: blocker, target: blocked})
691 |> put_status(:forbidden)
692 |> json(%{error: message})
696 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
697 with %User{} = blocked <- User.get_cached_by_id(id),
698 {:ok, blocker} <- User.unblock(blocker, blocked),
699 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
701 |> put_view(AccountView)
702 |> render("relationship.json", %{user: blocker, target: blocked})
706 |> put_status(:forbidden)
707 |> json(%{error: message})
711 def blocks(%{assigns: %{user: user}} = conn, _) do
712 with blocked_accounts <- User.blocked_users(user) do
713 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
718 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
719 with %User{} = subscription_target <- User.get_cached_by_id(id),
720 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
722 |> put_view(AccountView)
723 |> render("relationship.json", %{user: user, target: subscription_target})
727 |> put_status(:forbidden)
728 |> json(%{error: message})
732 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
733 with %User{} = subscription_target <- User.get_cached_by_id(id),
734 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
736 |> put_view(AccountView)
737 |> render("relationship.json", %{user: user, target: subscription_target})
741 |> put_status(:forbidden)
742 |> json(%{error: message})
746 def favourites(%{assigns: %{user: user}} = conn, params) do
749 |> Map.put("type", "Create")
750 |> Map.put("favorited_by", user.ap_id)
751 |> Map.put("blocking_user", user)
754 ActivityPub.fetch_activities([], params)
758 |> add_link_headers(activities)
759 |> put_view(StatusView)
760 |> render("index.json", %{activities: activities, for: user, as: :activity})
763 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
764 with %User{} = user <- User.get_by_id(id),
765 false <- user.info.hide_favorites do
768 |> Map.put("type", "Create")
769 |> Map.put("favorited_by", user.ap_id)
770 |> Map.put("blocking_user", for_user)
774 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
776 [Pleroma.Constants.as_public()]
781 |> ActivityPub.fetch_activities(params)
785 |> add_link_headers(activities)
786 |> put_view(StatusView)
787 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
789 nil -> {:error, :not_found}
790 true -> render_error(conn, :forbidden, "Can't get favorites")
794 def bookmarks(%{assigns: %{user: user}} = conn, params) do
795 user = User.get_cached_by_id(user.id)
798 Bookmark.for_user_query(user.id)
799 |> Pagination.fetch_paginated(params)
803 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
806 |> add_link_headers(bookmarks)
807 |> put_view(StatusView)
808 |> render("index.json", %{activities: activities, for: user, as: :activity})
811 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
812 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
813 res = ListView.render("lists.json", lists: lists)
817 def index(%{assigns: %{user: user}} = conn, _params) do
818 token = get_session(conn, :oauth_token)
821 mastodon_emoji = mastodonized_emoji()
823 limit = Config.get([:instance, :limit])
826 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
831 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
834 domain: Pleroma.Web.Endpoint.host(),
837 unfollow_modal: false,
840 auto_play_gif: false,
841 display_sensitive_media: false,
842 reduce_motion: false,
843 max_toot_chars: limit,
844 mascot: User.get_mascot(user)["url"]
846 poll_limits: Config.get([:instance, :poll_limits]),
848 delete_others_notice: present?(user.info.is_moderator),
849 admin: present?(user.info.is_admin)
853 default_privacy: user.info.default_scope,
854 default_sensitive: false,
855 allow_content_types: Config.get([:instance, :allowed_post_formats])
857 media_attachments: %{
858 accept_content_types: [
874 user.info.settings ||
904 push_subscription: nil,
906 custom_emojis: mastodon_emoji,
913 |> put_view(MastodonView)
914 |> render("index.html", %{initial_state: initial_state})
917 |> put_session(:return_to, conn.request_path)
918 |> redirect(to: "/web/login")
922 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
923 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
928 |> put_status(:internal_server_error)
929 |> json(%{error: inspect(e)})
933 def login(%{assigns: %{user: %User{}}} = conn, _params) do
934 redirect(conn, to: local_mastodon_root_path(conn))
937 @doc "Local Mastodon FE login init action"
938 def login(conn, %{"code" => auth_token}) do
939 with {:ok, app} <- get_or_make_app(),
940 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
941 {:ok, token} <- Token.exchange_token(app, auth) do
943 |> put_session(:oauth_token, token.token)
944 |> redirect(to: local_mastodon_root_path(conn))
948 @doc "Local Mastodon FE callback action"
949 def login(conn, _) do
950 with {:ok, app} <- get_or_make_app() do
955 response_type: "code",
956 client_id: app.client_id,
958 scope: Enum.join(app.scopes, " ")
961 redirect(conn, to: path)
965 defp local_mastodon_root_path(conn) do
966 case get_session(conn, :return_to) do
968 mastodon_api_path(conn, :index, ["getting-started"])
971 delete_session(conn, :return_to)
976 defp get_or_make_app do
977 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
978 scopes = ["read", "write", "follow", "push"]
980 with %App{} = app <- Repo.get_by(App, find_attrs) do
982 if app.scopes == scopes do
986 |> Changeset.change(%{scopes: scopes})
994 App.register_changeset(
996 Map.put(find_attrs, :scopes, scopes)
1003 def logout(conn, _) do
1006 |> redirect(to: "/")
1009 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1010 Logger.debug("Unimplemented, returning unmodified relationship")
1012 with %User{} = target <- User.get_cached_by_id(id) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: user, target: target})
1019 def empty_array(conn, _) do
1020 Logger.debug("Unimplemented, returning an empty array")
1024 def empty_object(conn, _) do
1025 Logger.debug("Unimplemented, returning an empty object")
1029 def get_filters(%{assigns: %{user: user}} = conn, _) do
1030 filters = Filter.get_filters(user)
1031 res = FilterView.render("filters.json", filters: filters)
1036 %{assigns: %{user: user}} = conn,
1037 %{"phrase" => phrase, "context" => context} = params
1043 hide: Map.get(params, "irreversible", false),
1044 whole_word: Map.get(params, "boolean", true)
1048 {:ok, response} = Filter.create(query)
1049 res = FilterView.render("filter.json", filter: response)
1053 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1054 filter = Filter.get(filter_id, user)
1055 res = FilterView.render("filter.json", filter: filter)
1060 %{assigns: %{user: user}} = conn,
1061 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1065 filter_id: filter_id,
1068 hide: Map.get(params, "irreversible", nil),
1069 whole_word: Map.get(params, "boolean", true)
1073 {:ok, response} = Filter.update(query)
1074 res = FilterView.render("filter.json", filter: response)
1078 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1081 filter_id: filter_id
1084 {:ok, _} = Filter.delete(query)
1088 def suggestions(%{assigns: %{user: user}} = conn, _) do
1089 suggestions = Config.get(:suggestions)
1091 if Keyword.get(suggestions, :enabled, false) do
1092 api = Keyword.get(suggestions, :third_party_engine, "")
1093 timeout = Keyword.get(suggestions, :timeout, 5000)
1094 limit = Keyword.get(suggestions, :limit, 23)
1096 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1098 user = user.nickname
1102 |> String.replace("{{host}}", host)
1103 |> String.replace("{{user}}", user)
1105 with {:ok, %{status: 200, body: body}} <-
1106 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1107 {:ok, data} <- Jason.decode(body) do
1110 |> Enum.slice(0, limit)
1113 |> Map.put("id", fetch_suggestion_id(x))
1114 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1115 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1121 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1128 defp fetch_suggestion_id(attrs) do
1129 case User.get_or_fetch(attrs["acct"]) do
1130 {:ok, %User{id: id}} -> id
1135 def reports(%{assigns: %{user: user}} = conn, params) do
1136 case CommonAPI.report(user, params) do
1139 |> put_view(ReportView)
1140 |> try_render("report.json", %{activity: activity})
1144 |> put_status(:bad_request)
1145 |> json(%{error: err})
1149 def account_register(
1150 %{assigns: %{app: app}} = conn,
1151 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1159 "captcha_answer_data",
1163 |> Map.put("nickname", nickname)
1164 |> Map.put("fullname", params["fullname"] || nickname)
1165 |> Map.put("bio", params["bio"] || "")
1166 |> Map.put("confirm", params["password"])
1168 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1169 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1171 token_type: "Bearer",
1172 access_token: token.token,
1174 created_at: Token.Utils.format_created_at(token)
1179 |> put_status(:bad_request)
1184 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1185 render_error(conn, :bad_request, "Missing parameters")
1188 def account_register(conn, _) do
1189 render_error(conn, :forbidden, "Invalid credentials")
1192 def conversations(%{assigns: %{user: user}} = conn, params) do
1193 participations = Participation.for_user_with_last_activity_id(user, params)
1196 Enum.map(participations, fn participation ->
1197 ConversationView.render("participation.json", %{participation: participation, for: user})
1201 |> add_link_headers(participations)
1202 |> json(conversations)
1205 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1206 with %Participation{} = participation <-
1207 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1208 {:ok, participation} <- Participation.mark_as_read(participation) do
1209 participation_view =
1210 ConversationView.render("participation.json", %{participation: participation, for: user})
1213 |> json(participation_view)
1217 def password_reset(conn, params) do
1218 nickname_or_email = params["email"] || params["nickname"]
1220 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1222 |> put_status(:no_content)
1225 {:error, "unknown user"} ->
1226 send_resp(conn, :not_found, "")
1229 send_resp(conn, :bad_request, "")
1233 def account_confirmation_resend(conn, params) do
1234 nickname_or_email = params["email"] || params["nickname"]
1236 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1237 {:ok, _} <- User.try_send_confirmation_email(user) do
1239 |> json_response(:no_content, "")
1243 def try_render(conn, target, params)
1244 when is_binary(target) do
1245 case render(conn, target, params) do
1246 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1251 def try_render(conn, _, _) do
1252 render_error(conn, :not_implemented, "Can't display this activity")
1255 defp present?(nil), do: false
1256 defp present?(false), do: false
1257 defp present?(_), do: true