X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Fmastodon_api%2Fmastodon_api_controller.ex;h=d6aacd288d8914e37a86d6b8aa156077ba18c0f9;hb=a0c65bbd6c708b555f457bf24ec07d2d41c3fe4a;hp=fc0100b1e45e2bcb537245a45fc730fa16a289a8;hpb=f1e67bdc312ba16a37916024244d6cb9d4417c9e;p=akkoma diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index fc0100b1e..d6aacd288 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -11,9 +11,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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.Repo alias Pleroma.ScheduledActivity @@ -39,13 +39,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 - @httpoison Application.get_env(:pleroma, :httpoison) + plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register) + plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search]) + @local_mastodon_name "Mastodon-Local" action_fallback(:errors) @@ -107,13 +110,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> 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 @@ -122,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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) @@ -133,7 +155,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 @@ -142,8 +167,80 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 @@ -168,7 +265,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - @mastodon_api_level "2.6.5" + @mastodon_api_level "2.7.2" def masto_instance(conn, _params) do instance = Config.get(:instance) @@ -187,7 +284,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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) @@ -293,7 +391,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do activities = [user.ap_id | user.following] |> ActivityPub.fetch_activities(params) - |> ActivityPub.contain_timeline(user) |> Enum.reverse() conn @@ -400,6 +497,67 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + false -> + conn + |> put_status(404) + |> json(%{error: "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 -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + false -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + {:error, message} -> + conn + |> put_status(422) + |> 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 @@ -449,26 +607,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 @@ -481,17 +624,40 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else params = Map.drop(params, ["scheduled_at"]) - {:ok, activity} = - Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> - CommonAPI.post(user, params) - end) - - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + case get_cached_status_or_post(conn, params) do + {:ignore, message} -> + conn + |> put_status(422) + |> json(%{error: message}) + + {:error, message} -> + conn + |> put_status(422) + |> json(%{error: message}) + + {_, activity} -> + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end end end + defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do + idempotency_key = + case get_req_header(conn, "idempotency-key") do + [key] -> key + _ -> Ecto.UUID.generate() + end + + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.post(user, params) do + {:ok, activity} -> activity + {:error, message} -> {:ignore, message} + end + end) + end + def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) @@ -698,6 +864,41 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_embed(:info, info_changeset) + + {:ok, _user} = User.update_and_set_cache(user_changeset) + + conn + |> json(rendered) + else + conn + |> put_resp_content_type("application/json") + |> send_resp(415, Jason.encode!(%{"error" => "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}} <- Repo.get(Activity, id), %Object{data: %{"likes" => likes}} <- Object.normalize(object) do @@ -1000,114 +1201,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def status_search_query_with_gin(q, query) do - from([a, o] in q, - where: - fragment( - "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", - o.data, - ^query - ), - order_by: [desc: :id] - ) - end - - def status_search_query_with_rum(q, query) do - from([a, o] in q, - where: - fragment( - "? @@ plainto_tsquery('english', ?)", - o.fts_content, - ^query - ), - order_by: [fragment("? <=> now()::date", o.inserted_at)] - ) - 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, - limit: 20 - ) - - q = - if Pleroma.Config.get([:database, :rum_enabled]) do - status_search_query_with_rum(q, query) - else - status_search_query_with_gin(q, query) - end - - 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 @@ -1236,7 +1329,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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) @@ -1302,8 +1395,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) - flavour = get_user_flavour(user) - initial_state = %{ meta: %{ @@ -1320,8 +1411,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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) @@ -1389,7 +1481,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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) @@ -1412,43 +1504,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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)})) - 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 @@ -1559,7 +1614,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 } @@ -1647,7 +1702,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> String.replace("{{user}}", user) with {:ok, %{status: 200, body: body}} <- - @httpoison.get( + HTTP.get( url, [], adapter: [ @@ -1716,6 +1771,53 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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(400) + |> json(Jason.encode!(errors)) + end + end + + def account_register(%{assigns: %{app: _app}} = conn, _params) do + conn + |> put_status(400) + |> json(%{error: "Missing parameters"}) + end + + def account_register(conn, _) do + conn + |> put_status(403) + |> json(%{error: "Invalid credentials"}) + end + def conversations(%{assigns: %{user: user}} = conn, params) do participations = Participation.for_user_with_last_activity_id(user, params)