defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
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.Notification
alias Pleroma.Object
- alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
+ alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Stats
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.MediaProxy
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Scopes
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)
- @httpoison Application.get_env(:pleroma, :httpoison)
@local_mastodon_name "Mastodon-Local"
action_fallback(:errors)
def create_app(conn, params) do
- scopes = ControllerHelper.oauth_scopes(params, ["read"])
+ scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
user_params =
%{}
|> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
+ |> 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
end
end)
+ emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
+
+ user_info_emojis =
+ ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
+ |> Enum.dedup()
+
info_params =
- [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
+ [
+ :no_rich_text,
+ :locked,
+ :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, "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
_ -> :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)
CommonAPI.update(user)
end
- json(conn, AccountView.render("account.json", %{user: user, for: user}))
+ json(
+ conn,
+ AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
+ )
else
- _e ->
- conn
- |> put_status(403)
- |> json(%{error: "Invalid request"})
+ _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 <- Ecto.Changeset.change(user) |> Ecto.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 <- Ecto.Changeset.change(user) |> Ecto.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 <- Ecto.Changeset.change(user) |> Ecto.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 <- Ecto.Changeset.change(user) |> Ecto.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
- account = AccountView.render("account.json", %{user: user, for: user})
+ 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
account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account)
else
- _e ->
- conn
- |> put_status(404)
- |> json(%{error: "Can't find user"})
+ _e -> render_error(conn, :not_found, "Can't find user")
end
end
- @mastodon_api_level "2.5.0"
+ @mastodon_api_level "2.7.2"
def masto_instance(conn, _params) do
instance = Config.get(:instance)
languages: ["en"],
registrations: Pleroma.Config.get([:instance, :registrations_open]),
# Extra (not present in Mastodon):
- max_toot_chars: Keyword.get(instance, :limit)
+ max_toot_chars: Keyword.get(instance, :limit),
+ poll_limits: Keyword.get(instance, :poll_limits)
}
json(conn, response)
"static_url" => url,
"visible_in_picker" => true,
"url" => url,
- "tags" => tags
+ "tags" => tags,
+ # Assuming that a comma is authorized in the category name
+ "category" => (tags -- ["Custom"]) |> Enum.join(",")
}
end)
end
activities =
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
- |> ActivityPub.contain_timeline(user)
|> Enum.reverse()
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> add_link_headers(:home_timeline, activities)
|> put_view(StatusView)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> put_view(StatusView)
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_id(params["id"]),
- reading_user <- Repo.preload(reading_user, :bookmarks) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
+ params =
+ params
+ |> Map.put("tag", params["tagged"])
+
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> add_link_headers(:dm_timeline, activities)
|> put_view(StatusView)
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
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
end
end
+ def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Object{} = object <- Object.get_by_id(id),
+ %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
+ nil -> render_error(conn, :not_found, "Record not found")
+ 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
end
end
- def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
- when length(media_ids) > 0 do
- params =
- params
- |> Map.put("status", ".")
-
- post_status(conn, params)
- 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"])
- idempotency_key =
- case get_req_header(conn, "idempotency-key") do
- [key] -> key
- _ -> Ecto.UUID.generate()
- end
-
scheduled_at = params["scheduled_at"]
if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
else
params = Map.drop(params, ["scheduled_at"])
- {:ok, activity} =
- Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
- CommonAPI.post(user, params)
- end)
+ case CommonAPI.post(user, params) do
+ {:error, message} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ {:ok, activity} ->
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
end
end
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
- _e ->
- conn
- |> put_status(403)
- |> json(%{error: "Can't delete this post"})
+ _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
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
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
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
- else
- {:error, reason} ->
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
end
end
%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
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
%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
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
- else
- {:error, reason} ->
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
end
end
else
{:error, reason} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => reason}))
+ |> put_status(:forbidden)
+ |> json(%{"error" => reason})
end
end
else
{:error, reason} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => reason}))
+ |> put_status(:forbidden)
+ |> json(%{"error" => reason})
end
end
end
end
- def favourited_by(conn, %{"id" => id}) do
- with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+ 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
+ |> Ecto.Changeset.change()
+ |> Ecto.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{data: %{"object" => object}} <- Activity.get_by_id(id),
%Object{data: %{"likes" => likes}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^likes)
- users = Repo.all(q)
+
+ users =
+ Repo.all(q)
+ |> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
- |> render(AccountView, "accounts.json", %{users: users, as: :user})
+ |> render("accounts.json", %{for: user, users: users, as: :user})
else
_ -> json(conn, [])
end
end
- def reblogged_by(conn, %{"id" => id}) do
- with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+ def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
%Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^announces)
- users = Repo.all(q)
+
+ users =
+ Repo.all(q)
+ |> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
- |> render("accounts.json", %{users: users, as: :user})
+ |> render("accounts.json", %{for: user, users: users, as: :user})
else
_ -> json(conn, [])
end
conn
|> add_link_headers(:followers, followers, user)
|> put_view(AccountView)
- |> render("accounts.json", %{users: followers, as: :user})
+ |> render("accounts.json", %{for: for_user, users: followers, as: :user})
end
end
conn
|> add_link_headers(:following, followers, user)
|> put_view(AccountView)
- |> render("accounts.json", %{users: followers, as: :user})
+ |> render("accounts.json", %{for: for_user, users: followers, as: :user})
end
end
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
conn
|> put_view(AccountView)
- |> render("accounts.json", %{users: follow_requests, as: :user})
+ |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
end
end
- def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
+ 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) do
+ {:ok, muter} <- User.mute(muter, muted, notifications) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: muter, target: muted})
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
else
{:error, message} ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(403, Jason.encode!(%{"error" => message}))
+ |> put_status(:forbidden)
+ |> json(%{error: message})
end
end
- def status_search(user, query) do
- fetched =
- if Regex.match?(~r/https?:/, query) do
- with {:ok, object} <- Fetcher.fetch_object_from_id(query),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
- true <- Visibility.visible_for_user?(activity, user) do
- [activity]
- else
- _e -> []
- end
- end || []
-
- q =
- from(
- [a, o] in Activity.with_preloaded_object(Activity),
- where: fragment("?->>'type' = 'Create'", a.data),
- where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
- where:
- fragment(
- "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
- o.data,
- ^query
- ),
- limit: 20,
- order_by: [desc: :id]
- )
-
- Repo.all(q) ++ fetched
- end
-
- def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
-
- statuses = status_search(user, query)
-
- tags_path = Web.base_url() <> "/tag/"
-
- tags =
- query
- |> String.split()
- |> Enum.uniq()
- |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
- |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
- |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
-
- res = %{
- "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
- "statuses" =>
- StatusView.render("index.json", activities: statuses, for: user, as: :activity),
- "hashtags" => tags
- }
-
- json(conn, res)
- end
-
- def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
-
- statuses = status_search(user, query)
-
- tags =
- query
- |> String.split()
- |> Enum.uniq()
- |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
- |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
-
- res = %{
- "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
- "statuses" =>
- StatusView.render("index.json", activities: statuses, for: user, as: :activity),
- "hashtags" => tags
- }
-
- json(conn, res)
- end
-
- def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
-
- res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
-
- json(conn, res)
- end
-
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
ActivityPub.fetch_activities([], params)
|> Enum.reverse()
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
recipients =
if for_user do
- ["https://www.w3.org/ns/activitystreams#Public"] ++
- [for_user.ap_id | for_user.following]
+ [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
else
- ["https://www.w3.org/ns/activitystreams#Public"]
+ [Pleroma.Constants.as_public()]
end
activities =
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: for_user, as: :activity})
else
- nil ->
- {:error, :not_found}
-
- true ->
- conn
- |> put_status(403)
- |> json(%{error: "Can't get favorites"})
+ 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)
- user = Repo.preload(user, bookmarks: :activity)
bookmarks =
Bookmark.for_user_query(user.id)
activities =
bookmarks
- |> Enum.map(fn b -> b.activity end)
+ |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(:bookmarks, bookmarks)
res = ListView.render("list.json", list: list)
json(conn, res)
else
- _e ->
- conn
- |> put_status(404)
- |> json(%{error: "Record not found"})
+ _e -> render_error(conn, :not_found, "Record not found")
end
end
json(conn, %{})
else
_e ->
- json(conn, "error")
+ json(conn, dgettext("errors", "error"))
end
end
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
- %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
+ %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
{:ok, users} = Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
- |> render("accounts.json", %{users: users, as: :user})
+ |> render("accounts.json", %{for: user, users: users, as: :user})
end
end
json(conn, res)
else
_e ->
- json(conn, "error")
+ json(conn, dgettext("errors", "error"))
end
end
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
- user = Repo.preload(user, bookmarks: :activity)
-
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
else
- _e ->
- conn
- |> put_status(403)
- |> json(%{error: "Error."})
+ _e -> render_error(conn, :forbidden, "Error.")
end
end
accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
- flavour = get_user_flavour(user)
-
initial_state =
%{
meta: %{
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit,
- mascot: "/images/pleroma-fox-tan-smol.png"
+ mascot: User.get_mascot(user)["url"]
},
+ poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
delete_others_notice: present?(user.info.is_moderator),
admin: present?(user.info.is_admin)
conn
|> put_layout(false)
|> put_view(MastodonView)
- |> render("index.html", %{initial_state: initial_state, flavour: flavour})
+ |> render("index.html", %{initial_state: initial_state})
else
conn
|> put_session(:return_to, conn.request_path)
else
e ->
conn
- |> put_resp_content_type("application/json")
- |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
- end
- end
-
- @supported_flavours ["glitch", "vanilla"]
-
- def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
- when flavour in @supported_flavours do
- flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
-
- with changeset <- Ecto.Changeset.change(user),
- changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
- {:ok, user} <- User.update_and_set_cache(changeset),
- flavour <- user.info.flavour do
- json(conn, flavour)
- else
- e ->
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
+ |> put_status(:internal_server_error)
+ |> json(%{error: inspect(e)})
end
end
- def set_flavour(conn, _params) do
- conn
- |> put_status(400)
- |> json(%{error: "Unsupported flavour"})
- end
-
- def get_flavour(%{assigns: %{user: user}} = conn, _params) do
- json(conn, get_user_flavour(user))
- end
-
- defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
- flavour
- end
-
- defp get_user_flavour(_) do
- "glitch"
- end
-
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
end
user_id: user.id,
phrase: phrase,
context: context,
- hide: Map.get(params, "irreversible", nil),
+ hide: Map.get(params, "irreversible", false),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
|> Enum.map_join(", ", fn {_k, v} -> v end)
conn
- |> put_status(422)
+ |> put_status(:unprocessable_entity)
|> json(%{error: error_message})
end
def errors(conn, {:error, :not_found}) do
+ render_error(conn, :not_found, "Record not found")
+ end
+
+ def errors(conn, {:error, error_message}) do
conn
- |> put_status(404)
- |> json(%{error: "Record not found"})
+ |> put_status(:bad_request)
+ |> json(%{error: error_message})
end
def errors(conn, _) do
conn
- |> put_status(500)
- |> json("Something went wrong")
+ |> put_status(:internal_server_error)
+ |> json(dgettext("errors", "Something went wrong"))
end
def suggestions(%{assigns: %{user: user}} = conn, _) do
|> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
- @httpoison.get(
+ HTTP.get(
url,
[],
adapter: [
end
end
- def try_render(conn, target, params)
- when is_binary(target) do
- res = render(conn, target, params)
+ 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
- if res == nil do
+ 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, user: user})
+ end)
+
+ conn
+ |> add_link_headers(:conversations, 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, user: user})
+
+ conn
+ |> json(participation_view)
+ end
+ end
+
+ def password_reset(conn, params) do
+ nickname_or_email = params["email"] || params["nickname"]
+
+ with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
conn
- |> put_status(501)
- |> json(%{error: "Can't display this activity"})
+ |> put_status(:no_content)
+ |> json("")
else
- res
+ {:error, "unknown user"} ->
+ send_resp(conn, :not_found, "")
+
+ {:error, _} ->
+ send_resp(conn, :bad_request, "")
+ 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
- conn
- |> put_status(501)
- |> json(%{error: "Can't display this activity"})
+ render_error(conn, :not_implemented, "Can't display this activity")
end
defp present?(nil), do: false