defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper,
- only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
- alias Ecto.Changeset
- alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
- alias Pleroma.Conversation.Participation
- alias Pleroma.Filter
- alias Pleroma.Formatter
alias Pleroma.HTTP
- alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
- alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.AppView
- alias Pleroma.Web.MastodonAPI.ConversationView
- alias Pleroma.Web.MastodonAPI.FilterView
- alias Pleroma.Web.MastodonAPI.ListView
- alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView
- alias Pleroma.Web.MastodonAPI.ReportView
- alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
- alias Pleroma.Web.ControllerHelper
- import Ecto.Query
-
require Logger
- require Pleroma.Constants
-
- @rate_limited_relations_actions ~w(follow unfollow)a
-
- @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
- post_status delete_status)a
-
- plug(
- RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
- when action in ~w(reblog_status unreblog_status)a
- )
- plug(
- RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
- when action in ~w(fav_status unfav_status)a
- )
-
- plug(
- RateLimiter,
- {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
- )
-
- plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
- plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
- plug(RateLimiter, :app_account_creation when action == :account_register)
- plug(RateLimiter, :search when action in [:search, :search2, :account_search])
plug(RateLimiter, :password_reset when action == :password_reset)
- plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
@local_mastodon_name "Mastodon-Local"
end
end
- defp add_if_present(
- map,
- params,
- params_field,
- map_field,
- value_function \\ fn x -> {:ok, x} end
- ) do
- if Map.has_key?(params, params_field) do
- case value_function.(params[params_field]) do
- {:ok, new_value} -> Map.put(map, map_field, new_value)
- :error -> map
- end
- else
- map
- end
- end
-
- def update_credentials(%{assigns: %{user: user}} = conn, params) do
- original_user = user
-
- user_params =
- %{}
- |> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
- |> add_if_present(params, "avatar", :avatar, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
- {:ok, object.data}
- else
- _ -> :error
- end
- end)
-
- emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
-
- user_info_emojis =
- user.info
- |> Map.get(:emoji, [])
- |> Enum.concat(Formatter.get_emoji_map(emojis_text))
- |> Enum.dedup()
-
- info_params =
- [
- :no_rich_text,
- :locked,
- :hide_followers_count,
- :hide_follows_count,
- :hide_followers,
- :hide_follows,
- :hide_favorites,
- :show_role,
- :skip_thread_containment
- ]
- |> Enum.reduce(%{}, fn key, acc ->
- add_if_present(acc, params, to_string(key), key, fn value ->
- {:ok, ControllerHelper.truthy_param?(value)}
- end)
- end)
- |> add_if_present(params, "default_scope", :default_scope)
- |> add_if_present(params, "fields", :fields, fn fields ->
- fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
-
- {:ok, fields}
- end)
- |> add_if_present(params, "fields", :raw_fields)
- |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
- {:ok, Map.merge(user.info.pleroma_settings_store, value)}
- end)
- |> add_if_present(params, "header", :banner, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :banner) do
- {:ok, object.data}
- else
- _ -> :error
- end
- end)
- |> add_if_present(params, "pleroma_background_image", :background, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :background) do
- {:ok, object.data}
- else
- _ -> :error
- end
- end)
- |> Map.put(:emoji, user_info_emojis)
-
- info_cng = User.Info.profile_update(user.info, info_params)
-
- with changeset <- User.update_changeset(user, user_params),
- changeset <- Changeset.put_embed(changeset, :info, info_cng),
- {:ok, user} <- User.update_and_set_cache(changeset) do
- if original_user != user do
- CommonAPI.update(user)
- end
-
- json(
- conn,
- AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
- )
- else
- _e -> render_error(conn, :forbidden, "Invalid request")
- end
- end
-
- def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- change = Changeset.change(user, %{avatar: nil})
- {:ok, user} = User.update_and_set_cache(change)
- CommonAPI.update(user)
-
- json(conn, %{url: nil})
- end
-
- def update_avatar(%{assigns: %{user: user}} = conn, params) do
- {:ok, object} = ActivityPub.upload(params, type: :avatar)
- change = Changeset.change(user, %{avatar: object.data})
- {:ok, user} = User.update_and_set_cache(change)
- CommonAPI.update(user)
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
-
- def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
- with new_info <- %{"banner" => %{}},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
- {:ok, user} <- User.update_and_set_cache(changeset) do
- CommonAPI.update(user)
-
- json(conn, %{url: nil})
- end
- end
-
- def update_banner(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
- new_info <- %{"banner" => object.data},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
- {:ok, user} <- User.update_and_set_cache(changeset) do
- CommonAPI.update(user)
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- with new_info <- %{"background" => %{}},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
- json(conn, %{url: nil})
- end
- end
-
- def update_background(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(params, type: :background),
- new_info <- %{"background" => object.data},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- def verify_credentials(%{assigns: %{user: user}} = conn, _) do
- chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
-
- account =
- AccountView.render("account.json", %{
- user: user,
- for: user,
- with_pleroma_settings: true,
- with_chat_token: chat_token
- })
-
- json(conn, account)
- end
-
def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
conn
end
end
- def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
- true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
- account = AccountView.render("account.json", %{user: user, for: for_user})
- json(conn, account)
- else
- _e -> render_error(conn, :not_found, "Can't find user")
- end
- end
-
@mastodon_api_level "2.7.2"
def masto_instance(conn, _params) do
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
- |> Enum.map(fn {shortcode, relative_url, tags} ->
+ |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
json(conn, mastodon_emoji)
end
- def home_timeline(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
-
- activities =
- [user.ap_id | user.following]
- |> ActivityPub.fetch_activities(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def public_timeline(%{assigns: %{user: user}} = conn, params) do
- local_only = params["local"] in [true, "True", "true", "1"]
-
- activities =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
- params =
- params
- |> Map.put("tag", params["tagged"])
-
- activities = ActivityPub.fetch_user_activities(user, reading_user, params)
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{
- activities: activities,
- for: reading_user,
- as: :activity
- })
- end
- end
-
- def dm_timeline(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put(:visibility, "direct")
-
- activities =
- [user.ap_id]
- |> ActivityPub.fetch_activities_query(params)
- |> Pagination.fetch_paginated(params)
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
- limit = 100
-
- activities =
- ids
- |> Enum.take(limit)
- |> Activity.all_by_ids_with_object()
- |> Enum.filter(&Visibility.visible_for_user?(&1, user))
-
- conn
- |> put_view(StatusView)
- |> render("index.json", activities: activities, for: user, as: :activity)
- end
-
- def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- true <- Visibility.visible_for_user?(activity, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user})
- end
- end
-
- def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id(id),
- activities <-
- ActivityPub.fetch_activities_for_context(activity.data["context"], %{
- "blocking_user" => user,
- "user" => user,
- "exclude_id" => activity.id
- }),
- grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
- result = %{
- ancestors:
- StatusView.render(
- "index.json",
- for: user,
- activities: grouped_activities[true] || [],
- as: :activity
- )
- |> Enum.reverse(),
- # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
- descendants:
- StatusView.render(
- "index.json",
- for: user,
- activities: grouped_activities[false] || [],
- as: :activity
- )
- |> Enum.reverse()
- # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
- }
-
- json(conn, result)
- end
- end
-
- def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
- true <- Visibility.visible_for_user?(activity, user) do
- conn
- |> put_view(StatusView)
- |> try_render("poll.json", %{object: object, for: user})
- else
- error when is_nil(error) or error == false ->
- render_error(conn, :not_found, "Record not found")
- end
- end
-
- defp get_cached_vote_or_vote(user, object, choices) do
- idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
-
- {_, res} =
- Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
- case CommonAPI.vote(user, object, choices) do
- {:error, _message} = res -> {:ignore, res}
- res -> {:commit, res}
- end
- end)
-
- res
- end
-
- def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
- with %Object{} = object <- Object.get_by_id(id),
- true <- object.data["type"] == "Question",
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
- conn
- |> put_view(StatusView)
- |> try_render("poll.json", %{object: object, for: user})
- else
- nil ->
- render_error(conn, :not_found, "Record not found")
-
- false ->
- render_error(conn, :not_found, "Record not found")
-
- {:error, message} ->
- conn
- |> put_status(:unprocessable_entity)
- |> json(%{error: message})
- end
- end
-
- def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
- with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
- conn
- |> add_link_headers(scheduled_activities)
- |> put_view(ScheduledActivityView)
- |> render("index.json", %{scheduled_activities: scheduled_activities})
- end
- end
-
- def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- _ -> {:error, :not_found}
- end
- end
-
- def update_scheduled_status(
- %{assigns: %{user: user}} = conn,
- %{"id" => scheduled_activity_id} = params
- ) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id),
- {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- nil -> {:error, :not_found}
- error -> error
- end
- end
-
- def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id),
- {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- nil -> {:error, :not_found}
- error -> error
- end
- end
-
- def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
- params =
- params
- |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
-
- scheduled_at = params["scheduled_at"]
-
- if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
- with {:ok, scheduled_activity} <-
- ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- end
- else
- params = Map.drop(params, ["scheduled_at"])
-
- case CommonAPI.post(user, params) do
- {:error, message} ->
- conn
- |> put_status(:unprocessable_entity)
- |> json(%{error: message})
-
- {:ok, activity} ->
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{
- activity: activity,
- for: user,
- as: :activity,
- with_direct_conversation_id: true
- })
- end
- end
- end
-
- def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- json(conn, %{})
- else
- _e -> render_error(conn, :forbidden, "Can't delete this post")
- end
- end
-
- def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
- %Activity{} = announce <- Activity.normalize(announce.data) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: announce, for: user, as: :activity})
- end
- end
-
- def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %User{} = user <- User.get_cached_by_nickname(user.nickname),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %User{} = user <- User.get_cached_by_nickname(user.nickname),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- activity = Activity.get_by_id(id)
-
- with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- activity = Activity.get_by_id(id)
-
- with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- id = List.wrap(id)
- q = from(u in User, where: u.id in ^id)
- targets = Repo.all(q)
-
- conn
- |> put_view(AccountView)
- |> render("relationships.json", %{user: user, targets: targets})
- end
-
- # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
- def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
-
- def update_media(%{assigns: %{user: user}} = conn, data) do
- with %Object{} = object <- Repo.get(Object, data["id"]),
- true <- Object.authorize_mutation(object, user),
- true <- is_binary(data["description"]),
- description <- data["description"] do
- new_data = %{object.data | "name" => description}
-
- {:ok, _} =
- object
- |> Object.change(%{data: new_data})
- |> Repo.update()
-
- attachment_data = Map.put(new_data, "id", object.id)
-
- conn
- |> put_view(StatusView)
- |> render("attachment.json", %{attachment: attachment_data})
- end
- end
-
- def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
- with {:ok, object} <-
- ActivityPub.upload(
- file,
- actor: User.ap_id(user),
- description: Map.get(data, "description")
- ) do
- attachment_data = Map.put(object.data, "id", object.id)
-
- conn
- |> put_view(StatusView)
- |> render("attachment.json", %{attachment: attachment_data})
- end
- end
-
- def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
- with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
- %{} = attachment_data <- Map.put(object.data, "id", object.id),
- %{type: type} = rendered <-
- StatusView.render("attachment.json", %{attachment: attachment_data}) do
- # Reject if not an image
- if type == "image" do
- # Sure!
- # Save to the user's info
- info_changeset = User.Info.mascot_update(user.info, rendered)
-
- user_changeset =
- user
- |> Changeset.change()
- |> Changeset.put_embed(:info, info_changeset)
-
- {:ok, _user} = User.update_and_set_cache(user_changeset)
-
- conn
- |> json(rendered)
- else
- render_error(conn, :unsupported_media_type, "mascots can only be images")
- end
- end
- end
-
- def get_mascot(%{assigns: %{user: user}} = conn, _params) do
- mascot = User.get_mascot(user)
-
- conn
- |> json(mascot)
- end
-
- def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
- %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
- q = from(u in User, where: u.ap_id in ^likes)
-
- users =
- Repo.all(q)
- |> Enum.filter(&(not User.blocks?(user, &1)))
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, users: users, as: :user})
- else
- {:visible, false} -> {:error, :not_found}
- _ -> json(conn, [])
- end
- end
-
- def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
- %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
- q = from(u in User, where: u.ap_id in ^announces)
-
- users =
- Repo.all(q)
- |> Enum.filter(&(not User.blocks?(user, &1)))
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, users: users, as: :user})
- else
- {:visible, false} -> {:error, :not_found}
- _ -> json(conn, [])
- end
- end
-
- def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
- local_only = params["local"] in [true, "True", "true", "1"]
-
- tags =
- [params["tag"], params["any"]]
- |> List.flatten()
- |> Enum.uniq()
- |> Enum.filter(& &1)
- |> Enum.map(&String.downcase(&1))
-
- tag_all =
- params["all"] ||
- []
- |> Enum.map(&String.downcase(&1))
-
- tag_reject =
- params["none"] ||
- []
- |> Enum.map(&String.downcase(&1))
-
- activities =
- params
- |> Map.put("type", "Create")
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> Map.put("tag", tags)
- |> Map.put("tag_all", tag_all)
- |> Map.put("tag_reject", tag_reject)
- |> ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- User.get_cached_by_id(id),
- followers <- MastodonAPI.get_followers(user, params) do
- followers =
- cond do
- for_user && user.id == for_user.id -> followers
- user.info.hide_followers -> []
- true -> followers
- end
-
- conn
- |> add_link_headers(followers)
- |> put_view(AccountView)
- |> render("accounts.json", %{for: for_user, users: followers, as: :user})
- end
- end
-
- def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- User.get_cached_by_id(id),
- followers <- MastodonAPI.get_friends(user, params) do
- followers =
- cond do
- for_user && user.id == for_user.id -> followers
- user.info.hide_follows -> []
- true -> followers
- end
-
- conn
- |> add_link_headers(followers)
- |> put_view(AccountView)
- |> render("accounts.json", %{for: for_user, users: followers, as: :user})
- end
- end
-
- def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
- with {:ok, follow_requests} <- User.get_follow_requests(followed) do
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
- end
- end
-
- def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- User.get_cached_by_id(id),
- {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: followed, target: follower})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- User.get_cached_by_id(id),
- {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: followed, target: follower})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: follower, target: followed})
- else
- {:followed, _} ->
- {:error, :not_found}
-
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
+ def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn
|> put_view(AccountView)
- |> render("account.json", %{user: followed, for: follower})
+ |> render("show.json", %{user: followed, for: follower})
else
{:followed, _} ->
{:error, :not_found}
end
end
- def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: follower, target: followed})
- else
- {:followed, _} ->
- {:error, :not_found}
-
- error ->
- error
- end
- end
-
- def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
- notifications =
- if Map.has_key?(params, "notifications"),
- do: params["notifications"] in [true, "True", "true", "1"],
- else: true
-
- with %User{} = muted <- User.get_cached_by_id(id),
- {:ok, muter} <- User.mute(muter, muted, notifications) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: muter, target: muted})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
- with %User{} = muted <- User.get_cached_by_id(id),
- {:ok, muter} <- User.unmute(muter, muted) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: muter, target: muted})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
def mutes(%{assigns: %{user: user}} = conn, _) do
with muted_accounts <- User.muted_users(user) do
- res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
+ res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
json(conn, res)
end
end
- def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
- with %User{} = blocked <- User.get_cached_by_id(id),
- {:ok, blocker} <- User.block(blocker, blocked),
- {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: blocker, target: blocked})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
- with %User{} = blocked <- User.get_cached_by_id(id),
- {:ok, blocker} <- User.unblock(blocker, blocked),
- {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: blocker, target: blocked})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_accounts <- User.blocked_users(user) do
- res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
+ res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
json(conn, res)
end
end
- def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
- json(conn, info.domain_blocks || [])
- end
-
- def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
- User.block_domain(blocker, domain)
- json(conn, %{})
- end
-
- def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
- User.unblock_domain(blocker, domain)
- json(conn, %{})
- end
-
- def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %User{} = subscription_target <- User.get_cached_by_id(id),
- {:ok, subscription_target} = User.subscribe(user, subscription_target) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: user, target: subscription_target})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %User{} = subscription_target <- User.get_cached_by_id(id),
- {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: user, target: subscription_target})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
- def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- User.get_by_id(id),
- false <- user.info.hide_favorites do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("favorited_by", user.ap_id)
- |> Map.put("blocking_user", for_user)
-
- recipients =
- if for_user do
- [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
- else
- [Pleroma.Constants.as_public()]
- end
-
- activities =
- recipients
- |> ActivityPub.fetch_activities(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: for_user, as: :activity})
- else
- nil -> {:error, :not_found}
- true -> render_error(conn, :forbidden, "Can't get favorites")
- end
- end
-
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
- def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
- lists = Pleroma.List.get_lists_account_belongs(user, account_id)
- res = ListView.render("lists.json", lists: lists)
- json(conn, res)
- end
-
- def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
- with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put("muting_user", user)
-
- # we must filter the following list for the user to avoid leaking statuses the user
- # does not actually have permission to see (for more info, peruse security issue #270).
- activities =
- following
- |> Enum.filter(fn x -> x in user.following end)
- |> ActivityPub.fetch_activities_bounded(following, params)
- |> Enum.reverse()
-
- conn
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- else
- _e -> render_error(conn, :forbidden, "Error.")
- end
- end
-
def index(%{assigns: %{user: user}} = conn, _params) do
token = get_session(conn, :oauth_token)
limit = Config.get([:instance, :limit])
- accounts =
- Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
+ accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
initial_state =
%{
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
- info_cng = User.Info.mastodon_settings_update(user.info, settings)
-
- with changeset <- Changeset.change(user),
- changeset <- Changeset.put_embed(changeset, :info, info_cng),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
+ with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
json(conn, %{})
else
e ->
@doc "Local Mastodon FE login init action"
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
- %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
+ {:ok, auth} <- Authorization.get_by_token(app, auth_token),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
- o_auth_path(
- conn,
- :authorize,
+ o_auth_path(conn, :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
end
end
+ @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do
- find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
- scopes = ["read", "write", "follow", "push"]
-
- with %App{} = app <- Repo.get_by(App, find_attrs) do
- {:ok, app} =
- if app.scopes == scopes do
- {:ok, app}
- else
- app
- |> Changeset.change(%{scopes: scopes})
- |> Repo.update()
- end
-
- {:ok, app}
- else
- _e ->
- cs =
- App.register_changeset(
- %App{},
- Map.put(find_attrs, :scopes, scopes)
- )
-
- Repo.insert(cs)
- end
+ App.get_or_make(
+ %{client_name: @local_mastodon_name, redirect_uris: "."},
+ ["read", "write", "follow", "push"]
+ )
end
def logout(conn, _) do
|> redirect(to: "/")
end
- def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- Logger.debug("Unimplemented, returning unmodified relationship")
-
- with %User{} = target <- User.get_cached_by_id(id) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: user, target: target})
- end
- end
-
+ # Stubs for unimplemented mastodon api
+ #
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
json(conn, [])
json(conn, %{})
end
- def get_filters(%{assigns: %{user: user}} = conn, _) do
- filters = Filter.get_filters(user)
- res = FilterView.render("filters.json", filters: filters)
- json(conn, res)
- end
-
- def create_filter(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context} = params
- ) do
- query = %Filter{
- user_id: user.id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", false),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.create(query)
- res = FilterView.render("filter.json", filter: response)
- json(conn, res)
- end
-
- def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
- filter = Filter.get(filter_id, user)
- res = FilterView.render("filter.json", filter: filter)
- json(conn, res)
- end
-
- def update_filter(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
- ) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", nil),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.update(query)
- res = FilterView.render("filter.json", filter: response)
- json(conn, res)
- end
-
- def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id
- }
-
- {:ok, _} = Filter.delete(query)
- json(conn, %{})
- end
-
def suggestions(%{assigns: %{user: user}} = conn, _) do
suggestions = Config.get(:suggestions)
end
end
- def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
- with %Activity{} = activity <- Activity.get_by_id(status_id),
- true <- Visibility.visible_for_user?(activity, user) do
- data =
- StatusView.render(
- "card.json",
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- )
-
- json(conn, data)
- else
- _e ->
- %{}
- end
- end
-
- def reports(%{assigns: %{user: user}} = conn, params) do
- case CommonAPI.report(user, params) do
- {:ok, activity} ->
- conn
- |> put_view(ReportView)
- |> try_render("report.json", %{activity: activity})
-
- {:error, err} ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: err})
- end
- end
-
- def account_register(
- %{assigns: %{app: app}} = conn,
- %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
- ) do
- params =
- params
- |> Map.take([
- "email",
- "captcha_solution",
- "captcha_token",
- "captcha_answer_data",
- "token",
- "password"
- ])
- |> Map.put("nickname", nickname)
- |> Map.put("fullname", params["fullname"] || nickname)
- |> Map.put("bio", params["bio"] || "")
- |> Map.put("confirm", params["password"])
-
- with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
- {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
- json(conn, %{
- token_type: "Bearer",
- access_token: token.token,
- scope: app.scopes,
- created_at: Token.Utils.format_created_at(token)
- })
- else
- {:error, errors} ->
- conn
- |> put_status(:bad_request)
- |> json(errors)
- end
- end
-
- def account_register(%{assigns: %{app: _app}} = conn, _params) do
- render_error(conn, :bad_request, "Missing parameters")
- end
-
- def account_register(conn, _) do
- render_error(conn, :forbidden, "Invalid credentials")
- end
-
- def conversations(%{assigns: %{user: user}} = conn, params) do
- participations = Participation.for_user_with_last_activity_id(user, params)
-
- conversations =
- Enum.map(participations, fn participation ->
- ConversationView.render("participation.json", %{participation: participation, for: user})
- end)
-
- conn
- |> add_link_headers(participations)
- |> json(conversations)
- end
-
- def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
- with %Participation{} = participation <-
- Repo.get_by(Participation, id: participation_id, user_id: user.id),
- {:ok, participation} <- Participation.mark_as_read(participation) do
- participation_view =
- ConversationView.render("participation.json", %{participation: participation, for: user})
-
- conn
- |> json(participation_view)
- end
- end
-
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
end
end
- def account_confirmation_resend(conn, params) do
- nickname_or_email = params["email"] || params["nickname"]
-
- with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
- {:ok, _} <- User.try_send_confirmation_email(user) do
- conn
- |> json_response(:no_content, "")
- end
- end
-
- def try_render(conn, target, params)
- when is_binary(target) do
- case render(conn, target, params) do
- nil -> render_error(conn, :not_implemented, "Can't display this activity")
- res -> res
- end
- end
-
- def try_render(conn, _, _) do
- render_error(conn, :not_implemented, "Can't display this activity")
- end
-
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true