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(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
554 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
555 {_, true} <- {:followed, follower.id != followed.id},
556 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
558 |> put_view(AccountView)
559 |> render("relationship.json", %{user: follower, target: followed})
566 |> put_status(:forbidden)
567 |> json(%{error: message})
571 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
572 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
573 {_, true} <- {:followed, follower.id != followed.id},
574 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
576 |> put_view(AccountView)
577 |> render("account.json", %{user: followed, for: follower})
584 |> put_status(:forbidden)
585 |> json(%{error: message})
589 def unfollow(%{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} <- CommonAPI.unfollow(follower, followed) do
594 |> put_view(AccountView)
595 |> render("relationship.json", %{user: follower, target: followed})
605 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
607 if Map.has_key?(params, "notifications"),
608 do: params["notifications"] in [true, "True", "true", "1"],
611 with %User{} = muted <- User.get_cached_by_id(id),
612 {:ok, muter} <- User.mute(muter, muted, notifications) do
614 |> put_view(AccountView)
615 |> render("relationship.json", %{user: muter, target: muted})
619 |> put_status(:forbidden)
620 |> json(%{error: message})
624 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
625 with %User{} = muted <- User.get_cached_by_id(id),
626 {:ok, muter} <- User.unmute(muter, muted) do
628 |> put_view(AccountView)
629 |> render("relationship.json", %{user: muter, target: muted})
633 |> put_status(:forbidden)
634 |> json(%{error: message})
638 def mutes(%{assigns: %{user: user}} = conn, _) do
639 with muted_accounts <- User.muted_users(user) do
640 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
645 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
646 with %User{} = blocked <- User.get_cached_by_id(id),
647 {:ok, blocker} <- User.block(blocker, blocked),
648 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
650 |> put_view(AccountView)
651 |> render("relationship.json", %{user: blocker, target: blocked})
655 |> put_status(:forbidden)
656 |> json(%{error: message})
660 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
661 with %User{} = blocked <- User.get_cached_by_id(id),
662 {:ok, blocker} <- User.unblock(blocker, blocked),
663 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
665 |> put_view(AccountView)
666 |> render("relationship.json", %{user: blocker, target: blocked})
670 |> put_status(:forbidden)
671 |> json(%{error: message})
675 def blocks(%{assigns: %{user: user}} = conn, _) do
676 with blocked_accounts <- User.blocked_users(user) do
677 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
682 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
683 json(conn, info.domain_blocks || [])
686 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
687 User.block_domain(blocker, domain)
691 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
692 User.unblock_domain(blocker, domain)
696 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
697 with %User{} = subscription_target <- User.get_cached_by_id(id),
698 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
700 |> put_view(AccountView)
701 |> render("relationship.json", %{user: user, target: subscription_target})
705 |> put_status(:forbidden)
706 |> json(%{error: message})
710 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
711 with %User{} = subscription_target <- User.get_cached_by_id(id),
712 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
714 |> put_view(AccountView)
715 |> render("relationship.json", %{user: user, target: subscription_target})
719 |> put_status(:forbidden)
720 |> json(%{error: message})
724 def favourites(%{assigns: %{user: user}} = conn, params) do
727 |> Map.put("type", "Create")
728 |> Map.put("favorited_by", user.ap_id)
729 |> Map.put("blocking_user", user)
732 ActivityPub.fetch_activities([], params)
736 |> add_link_headers(activities)
737 |> put_view(StatusView)
738 |> render("index.json", %{activities: activities, for: user, as: :activity})
741 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
742 with %User{} = user <- User.get_by_id(id),
743 false <- user.info.hide_favorites do
746 |> Map.put("type", "Create")
747 |> Map.put("favorited_by", user.ap_id)
748 |> Map.put("blocking_user", for_user)
752 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
754 [Pleroma.Constants.as_public()]
759 |> ActivityPub.fetch_activities(params)
763 |> add_link_headers(activities)
764 |> put_view(StatusView)
765 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
767 nil -> {:error, :not_found}
768 true -> render_error(conn, :forbidden, "Can't get favorites")
772 def bookmarks(%{assigns: %{user: user}} = conn, params) do
773 user = User.get_cached_by_id(user.id)
776 Bookmark.for_user_query(user.id)
777 |> Pagination.fetch_paginated(params)
781 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
784 |> add_link_headers(bookmarks)
785 |> put_view(StatusView)
786 |> render("index.json", %{activities: activities, for: user, as: :activity})
789 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
790 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
791 res = ListView.render("lists.json", lists: lists)
795 def index(%{assigns: %{user: user}} = conn, _params) do
796 token = get_session(conn, :oauth_token)
799 mastodon_emoji = mastodonized_emoji()
801 limit = Config.get([:instance, :limit])
804 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
809 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
812 domain: Pleroma.Web.Endpoint.host(),
815 unfollow_modal: false,
818 auto_play_gif: false,
819 display_sensitive_media: false,
820 reduce_motion: false,
821 max_toot_chars: limit,
822 mascot: User.get_mascot(user)["url"]
824 poll_limits: Config.get([:instance, :poll_limits]),
826 delete_others_notice: present?(user.info.is_moderator),
827 admin: present?(user.info.is_admin)
831 default_privacy: user.info.default_scope,
832 default_sensitive: false,
833 allow_content_types: Config.get([:instance, :allowed_post_formats])
835 media_attachments: %{
836 accept_content_types: [
852 user.info.settings ||
882 push_subscription: nil,
884 custom_emojis: mastodon_emoji,
891 |> put_view(MastodonView)
892 |> render("index.html", %{initial_state: initial_state})
895 |> put_session(:return_to, conn.request_path)
896 |> redirect(to: "/web/login")
900 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
901 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
906 |> put_status(:internal_server_error)
907 |> json(%{error: inspect(e)})
911 def login(%{assigns: %{user: %User{}}} = conn, _params) do
912 redirect(conn, to: local_mastodon_root_path(conn))
915 @doc "Local Mastodon FE login init action"
916 def login(conn, %{"code" => auth_token}) do
917 with {:ok, app} <- get_or_make_app(),
918 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
919 {:ok, token} <- Token.exchange_token(app, auth) do
921 |> put_session(:oauth_token, token.token)
922 |> redirect(to: local_mastodon_root_path(conn))
926 @doc "Local Mastodon FE callback action"
927 def login(conn, _) do
928 with {:ok, app} <- get_or_make_app() do
933 response_type: "code",
934 client_id: app.client_id,
936 scope: Enum.join(app.scopes, " ")
939 redirect(conn, to: path)
943 defp local_mastodon_root_path(conn) do
944 case get_session(conn, :return_to) do
946 mastodon_api_path(conn, :index, ["getting-started"])
949 delete_session(conn, :return_to)
954 defp get_or_make_app do
955 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
956 scopes = ["read", "write", "follow", "push"]
958 with %App{} = app <- Repo.get_by(App, find_attrs) do
960 if app.scopes == scopes do
964 |> Changeset.change(%{scopes: scopes})
972 App.register_changeset(
974 Map.put(find_attrs, :scopes, scopes)
981 def logout(conn, _) do
987 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
988 Logger.debug("Unimplemented, returning unmodified relationship")
990 with %User{} = target <- User.get_cached_by_id(id) do
992 |> put_view(AccountView)
993 |> render("relationship.json", %{user: user, target: target})
997 def empty_array(conn, _) do
998 Logger.debug("Unimplemented, returning an empty array")
1002 def empty_object(conn, _) do
1003 Logger.debug("Unimplemented, returning an empty object")
1007 def get_filters(%{assigns: %{user: user}} = conn, _) do
1008 filters = Filter.get_filters(user)
1009 res = FilterView.render("filters.json", filters: filters)
1014 %{assigns: %{user: user}} = conn,
1015 %{"phrase" => phrase, "context" => context} = params
1021 hide: Map.get(params, "irreversible", false),
1022 whole_word: Map.get(params, "boolean", true)
1026 {:ok, response} = Filter.create(query)
1027 res = FilterView.render("filter.json", filter: response)
1031 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1032 filter = Filter.get(filter_id, user)
1033 res = FilterView.render("filter.json", filter: filter)
1038 %{assigns: %{user: user}} = conn,
1039 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1043 filter_id: filter_id,
1046 hide: Map.get(params, "irreversible", nil),
1047 whole_word: Map.get(params, "boolean", true)
1051 {:ok, response} = Filter.update(query)
1052 res = FilterView.render("filter.json", filter: response)
1056 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1059 filter_id: filter_id
1062 {:ok, _} = Filter.delete(query)
1066 def suggestions(%{assigns: %{user: user}} = conn, _) do
1067 suggestions = Config.get(:suggestions)
1069 if Keyword.get(suggestions, :enabled, false) do
1070 api = Keyword.get(suggestions, :third_party_engine, "")
1071 timeout = Keyword.get(suggestions, :timeout, 5000)
1072 limit = Keyword.get(suggestions, :limit, 23)
1074 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1076 user = user.nickname
1080 |> String.replace("{{host}}", host)
1081 |> String.replace("{{user}}", user)
1083 with {:ok, %{status: 200, body: body}} <-
1084 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1085 {:ok, data} <- Jason.decode(body) do
1088 |> Enum.slice(0, limit)
1091 |> Map.put("id", fetch_suggestion_id(x))
1092 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1093 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1099 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1106 defp fetch_suggestion_id(attrs) do
1107 case User.get_or_fetch(attrs["acct"]) do
1108 {:ok, %User{id: id}} -> id
1113 def reports(%{assigns: %{user: user}} = conn, params) do
1114 case CommonAPI.report(user, params) do
1117 |> put_view(ReportView)
1118 |> try_render("report.json", %{activity: activity})
1122 |> put_status(:bad_request)
1123 |> json(%{error: err})
1127 def account_register(
1128 %{assigns: %{app: app}} = conn,
1129 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1137 "captcha_answer_data",
1141 |> Map.put("nickname", nickname)
1142 |> Map.put("fullname", params["fullname"] || nickname)
1143 |> Map.put("bio", params["bio"] || "")
1144 |> Map.put("confirm", params["password"])
1146 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1147 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1149 token_type: "Bearer",
1150 access_token: token.token,
1152 created_at: Token.Utils.format_created_at(token)
1157 |> put_status(:bad_request)
1162 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1163 render_error(conn, :bad_request, "Missing parameters")
1166 def account_register(conn, _) do
1167 render_error(conn, :forbidden, "Invalid credentials")
1170 def conversations(%{assigns: %{user: user}} = conn, params) do
1171 participations = Participation.for_user_with_last_activity_id(user, params)
1174 Enum.map(participations, fn participation ->
1175 ConversationView.render("participation.json", %{participation: participation, for: user})
1179 |> add_link_headers(participations)
1180 |> json(conversations)
1183 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1184 with %Participation{} = participation <-
1185 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1186 {:ok, participation} <- Participation.mark_as_read(participation) do
1187 participation_view =
1188 ConversationView.render("participation.json", %{participation: participation, for: user})
1191 |> json(participation_view)
1195 def password_reset(conn, params) do
1196 nickname_or_email = params["email"] || params["nickname"]
1198 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1200 |> put_status(:no_content)
1203 {:error, "unknown user"} ->
1204 send_resp(conn, :not_found, "")
1207 send_resp(conn, :bad_request, "")
1211 def account_confirmation_resend(conn, params) do
1212 nickname_or_email = params["email"] || params["nickname"]
1214 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1215 {:ok, _} <- User.try_send_confirmation_email(user) do
1217 |> json_response(:no_content, "")
1221 def try_render(conn, target, params)
1222 when is_binary(target) do
1223 case render(conn, target, params) do
1224 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1229 def try_render(conn, _, _) do
1230 render_error(conn, :not_implemented, "Can't display this activity")
1233 defp present?(nil), do: false
1234 defp present?(false), do: false
1235 defp present?(_), do: true